Android View的工作原理
一、绘制流程
View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout、draw三个过程才能最终将一个View绘制出来,其中measure是用来测量View的宽高,layout是用来确定View在父容器的位置,draw则负责将View绘制在屏幕上,大致流程如下:
绘制流程.png二、measure过程
1、MeasureSpec
从上图可以了解到View在绘制过程中会调用到View的measure()方法,measure()方法接收两个参数:widthMeasureSpec和heightMeasureSpec,分别用于确定视图的宽度和高度的规格。
MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)。
SpecMode有三类:
-
UNSPECIFIED
未指定模式,父容器不对View有任何限制,一般用于系统内部,开发过程中不太会用到。 -
EXACTLY
精确模式,父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的match_parent和具体的数值这两种模式。 -
AT_MOST
最大模式,父容器指定了一个可用大小,即SpecSize,View的大小不能大于这个值。它对应LayoutParams中的wrap_content。
子视图的MeasureSpec
widthMeasureSpec和heightMeasureSpec这两个参数的值通常是由父视图传递给子视图,再经过计算得出来的,说明父视图会在一定程度上决定子视图的大小。观察ViewGroup的measureChildWithMargins方法如下:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
其中childWidthMeasureSpec 与childHeightMeasureSpec 都是通过getChildMeasureSpec的计算得出的,并且与父容器的MeasureSpec和子元素本身的LayoutParams有关,再看看getChildMeasureSpec方法的代码:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
以上的代码可以用一个表格来表示:
普通View的MeasureSpce创建规则.png总结如下:
- 当View采用固定宽/高时,不管父容器的Measure是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams中的大小。
- 当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
- 当View的宽/高是wrap_content时,不管父容器是最大模式还是精确模式,View的模式总是最大模式,并且其大小不会超过父容器的剩余空间。
- UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般来说,不需要关注此模式。
根视图的MeasureSpec
最外层的根视图的widthMeasureSpec和heightMeasureSpec是在performTraversals()方法中获取到:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
其中的lp.width和lp.height在创建ViewGroup实例的时候就被赋值为MATCH_PARENT了,getRootMeasureSpec的代码如下:
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
由此可见,当rootDimension等于MATCH_PARENT时,MeasureSpec的SpecMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT时,MeasureSpec的SpecMode就等于AT_MOST,当rootDimension为具体数值时,MeasureSpec的SpecMode就等于EXACTLY,与前面描述的一致。且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。
2、View的measure过程
View的measure过程由其measure方法来完成,而measure方法是一个final方法,这意味着子类不能重写此方法,而measure方法中调用的onMeasure方法才是真正去测量并设置View大小的地方,默认会调用getDefaultSize方法来获取视图的大小:
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;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这里的MeasureSpec是由measure方法传递下来的,测量后调用setMeasuredDimension方法来设定测量后的大小,这样一次measure过程就结束了,这是系统的默认测量方式,实际上我们可以重写这个方法来改变测量方式,从而实现自定义View的测量。
值得注意的是,在重写onMeasure方法的时候,需要注意设置好View的warp_content情况,按照自身情况来测量出实际所需大小,否则在布局中使用wrap_content就相当于使用match_parent,从代码可以看出,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,则宽/高等于specSize,从上面的“普通View的MeasureSpce创建规则”表中可知,这种情况下View的specSize是parentSize,即父容器当前剩余空间大小,与使用match_parent效果一致。因此需要根据需求来判断解决这个问题,例如使用默认大小等。
3、ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。与View不同的是,ViewGroup是一个抽象类,并没有定义其测量的具体过程,毕竟不同ViewGroup的子类有不同的布局特性,如RelativeLayout和LinearLayout,因此需要子类自己去实现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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild与measureChildWithMargins不同的地方在于,measureChild没有测量自己的margin属性,而measureChildWithMargins有,当需要使用到margin属性时,还是需要使用measureChildWithMargins来测量。
4、测量结束
measure完成后,通过getMeasuredWidth/getMeasuredHeight方法就可以正确地获取到View的测量宽/高,但是在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的,因为View需要多次measure才能确定自己的宽/高,前几次测量过程中,得出的测量结果可能与最终结果不一致,因此最好还是在onLayout方法中去获取View的测量宽/高或者最终宽/高。
三、layout过程
measure结束后,视图的大小就已经测量好了,接下来就是layout过程了。layout的作用是给视图进行布局的,也就是确定视图的位置。ViewRootd的performTraversals方法会在measure结束后继续执行,并调用layout方法来执行此过程:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout方法接收四个参数,分别代表着相对于当前视图的父视图而言的左、上、右、下的坐标,在layout中会调用onLayout方法,但是,View的onLayout是一个空方法,因为View的位置应该由父视图ViewGroup来决定的,而ViewGroup中的onLayout方法是一个抽象方法,这是由于每个ViewGroup的布局方式不同,因此需要重写这个方法来确定子元素的位置。
layout结束后,就可以通过getWidth和getHeight来得到其最终宽/高:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
四、draw过程
draw过程比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(disptchDraw)
- 绘制装饰(onDrawScrollBars)
首先绘制背景,其实就是在XML中通过android:background属性设置的图片或颜色,当然也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值;
接下来是绘制自己,调用onDraw方法,使用画布来绘制自己的内容,自定义View的时候主要就是重写这一个方法;
接下来是绘制children,调用disptchDraw来绘制所有的子元素;
最后是绘制装饰,这一步的作用是对视图的滚动条进行绘制,每一个View其实都有滚动条,只是有些控件没有显示出来。