在了解View工作原理之前,需要先了解一些基础概念:
- DecorView:顶层视图,将要显示的具体内容呈现在PhoneWindow上,是当前Activity所有View的根节点。
- ViewRootImpl:连接WindowManager和DecorView的纽带,View的绘制流程正是从通过ViewRootImpl来完成的。
(图片来源自:Android窗口机制)
一、View的绘制流程是从哪里开始
这可能得从Android的启动过程说起,但是此处不深究Android的启动过程,只是抽丝剥茧的取出其中与View的绘制过程关系较为直接的来说:
当Activity初始化Window和将布局添加到DecorView之后,ActivityThread类会调用handleResumeActivity会通过WindowManager将DecorView视图添加到窗口上,大致调用过程为:
ActivityThread.handleResumeActivity()
| WindowManager.addView()
|| WindowManagerGlobal.addView()
||| ViewRootImpl.setView() -> requestLayout() -> scheduleTraversals()
|||| TraversalRunnable.run() -> ViewRootImpl.doTraversal()
||||| ViewRootImpl.performTraversals()
关键点就是ViewRootImpl.performTraversals(),展开一看究竟
private void performTraversals(){
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
......
}
从上面的代码可以看出,performTraversals()依次调用了performMeasure、performLayout 和 performDraw方法,正是测量、布局、绘制。所以,View的绘制流程是从ViewRootImpl的performTraversals开始的。
performMeasure()会调用顶级View的measure()方法,measure又去调用onMeasure(),onMeasure中会对所有子元素进行measure过程;接着子元素会重复父容器的measure过程,如此反复至完成整个View树的遍历。layout和draw同理。
二、View绘制流程之measure
1. 理解MeasureSpec
在搞清楚measure过程之前,得先明白MeasureSpec的概念,因为在measure过程中,MeasureSpec将会贯穿其中。
MeasureSpec是一个32位int值,高2位是SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)
SpecMode的3种模式:
- UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
- EXACTLY(精确模式):父容器已测出View所需要的精确大小,View的尺寸就是父容器给的确切值。对应LyaoutParams中的match_parent或具体数值。
- AT_MOST(最大模式):父容器给出可用大小,View的大小不能超过该值,具体要看View的具体实现。对应LayoutParams中的wrap_content。
普通View的MeasureSpec的创建规则对照表:
parentSpecMode / chlidLayoutParams | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
其中竖列为chlidLayoutParams,横为parentSpecMode。
表格的推演由来,可参考:ViewGrop.getChildMeasureSpec()
2. measure过程
measure过程由measure完成,measure是final方法,不能被重写,所以重点看里面调用的onMeasure。容器类型的View,需要先测量子View。
- 单个View的测量:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置宽高的测量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// AT_MOST & EXACTLY的返回值是一样的
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从getDefaultSize可以看出,默认AT_MOST和EXACTLY的返回值是一样的,所以直接继承View的控件需要重写onMeasure设置specMode=wrap_content时的大小,不然wrap_content效果=match_parent。
- ViewGroup测量:
ViewGroup除了测量自身,还需要去调用所有子元素的measure方法来测量子元素,子元素在递归去执行该过程。
ViewGroup提供了measureChildren方法:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 调用子View的 measure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在measure时,遍历所有子元素,调用子元素的measure方法。虽然ViewGroup的子类容器布局不一定使用measureChildren方法,但是主要逻辑是一样的,都是遍历子View进行测量。
三、View绘制流程之layout
layout的作用是用来确定容器自身以及子元素的位置,首先调用layout()确定自身位置,layout中又会调用onLayout来确定子元素位置。
来一张图看layout的流程:
四、View绘制流程之draw
draw作用是将View绘制到屏幕上。
绘制顺序如下:
- 绘制背景:background.draw(canvas)
- 绘制自己:onDraw(canvas)
- 绘制children:dispatchDraw(canvas)
- 绘制装饰:onDrawScrollBars(canvas)
子View的绘制是通过dispatchDraw方法来往下传递的,它会遍历调用所有子元素的draw方法,所有都绘制完成后,在执行自身的onDrawScrollBars。
protected void dispatchDraw(Canvas canvas) {
......
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 调用 child.draw方法
more |= drawChild(canvas, transientChild, drawingTime);
}
......
}