上一篇文章描述了我们的在Activity中的onCreate中去setContentView,是怎样将布局显示在屏幕上的。setContentView都干了些什么 ,接着流程梳理,上篇结尾处说到了绘制的起始点,也就是ViewRootImpl的performTraversals()方法中的performMeasure、performLayout、performDraw。Android的绘制流程分为三步,第一步measure(测量),第二步layout(布局摆放),第三步draw(绘制)。很好理解这三个步骤,因为跟现实生活的场景很像,就像要布置一个新房子一样。
- 我们首先得知道这个屋子的大小,而且还要知道每个要放入屋子中的物品的大小。知道大小的过程就是Measure过程。
- 知道所有的大小尺寸后,根据他们的尺寸,我们才能去安排他们具体摆放在哪个位置。否则有些地方太小,物品太大,可能放不下。也有可能有的地方太大,物品太小,而浪费空间。所以根据物品大小,合理安排摆放位置。
- 在图纸上规划完所有物品的摆放之后,根据图纸上的位置,把真实的物品一个一个摆放在他们应该存在的位置上。
Measure(测量)
想要测量大小,最先应该测量的就应该是房子的大小吧。进入ViewRootImpl的performTraversals()方法,此方法超级长,有一段代码
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
看到两个获取宽高的的方法,都分别传入一个int类型的宽或者高的值,和一个对应的layoutParams的值。mWidth和mHeight此window的宽高,此处可以理解为屏幕的宽高。layoutParams为一个WindowParams,它的width和height都为 LayoutParams.MATCH_PARENT。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
很明显,代码会走ViewGroup.LayoutParams.MATCH_PARENT这个case。调用了MeasureSpec类的makeMeasureSpec静态方法,返回了一个int值。第一个参数,第二个参数竟然是MeasureSpec类的一个常量。看来这个类很重要,有必要先要了解下这个类。
MeasureSpec
此类是View的一个内部类。它提供给子View去测量的实体,它包含mode与size两部分。它内部的执行都是32位的位操作,可以看做仅仅对一个int值进行操作,极大减小了内存的分配。32位的最高两位代表着mode部分,后30位代表着具体数值。mode有三种模式,所以完全可以用2位来表示。它们分别是:
- UNSPECIFIED:此模式代表子View想要多大都行,父容器都不会干涉测量。这模式在自定义中很少用到。一般都是在系统控件中才用到,如ListView中。
- EXACTLY:父容器提供一个精确的值给子View。
- AT_MOST:子View的大小自己决定,但是最大不能超过父容器给定的值。
它还包含三个常用方法: - makeMeasureSpec(int size,int mode):将传入的size和mode组装成一个MeasureSpec返回
- getMode(int measureSpec):取出measureSpec中的mode值返回。
- getSize(int measureSpec):取出measureSpec中的size值返回。
回到ViewRootImpl的getRootMeasureSpec方法的第一个case中,这方法只是将size为屏幕宽度,mode为MeasureSpec.EXACTLY组装成了一个measureSpec返回。接着执行performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
此方法中执行了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
mView就是DecorView,所以进入measure()方法。发现直接到了View的measure()方法,因为此方法是被final修饰了,所以所有子类都不能重写此方法。那就查看此方法的逻辑,代码有一大部分是有关缓存的,其中执行了一句很重要的代码onMeasure(widthMeasureSpec, heightMeasureSpec);
onMeasure()是可以被重写的,所以要看看重写后的逻辑。此时的view是DecorView,它的继承体系是这样的
在DecorView中重写了onMeasure方法。在它的onMeasure中又调用了
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
进入到了FrameLayout的onMeasure中
//......
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//......
}
}
如果子view没有全部测量完毕或者当前的子view不是在Gone的状态下,就调用measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
从方法名可以很轻易的看出,这个方法是测量children的方法(带margin),进入该方法
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);
}
此方法是ViewGroup的方法,首先获取当前view的layoutParams,接着调用了getChildMeasureSpec方法返回了相应的MeasureSpec,那getChildMeasureSpec方法就是根据父容器提供的MeasureSpec,和父容器的padding值,和自己的margin值和自己的layoutParams来生成一个MeasureSpec。查看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);
}
逻辑很清晰,分别获取父容器提供的MeasureSpec中的mode和size。定义了resultSize 、resultMode 来进行计算,最后通过return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
将resultSize 和resultMode 组装成一个MeasureSpec返回。分别进入三个case中
- MeasureSpec.EXACTLY:当父容器提供了一个精确的值给子View。由于我们在android:layout_width=" "中只能写match_parent,wrap_content。所以分这三种情况进行判断。
- childDimension > 0:由于LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT定义的常量分别为-1,-2.所以只要childDimension >0就一定是定义了固定的数值。既然子View定义了固定的数值,那么resultSize就应该是它固定的值。resultMode就应该为MeasureSpec.EXACTLY,精确模式。
- childDimension == LayoutParams.MATCH_PARENT:如果子View想要的是MATCH_PARENT,那么resultSize应该等于父容器能提供的大小。这也是一个精确的值,所以resultMode应该为MeasureSpec.EXACTLY,精确模式。
- childDimension == LayoutParams.WRAP_CONTENT:如果子View只是想要包裹自己的内容。那现在是没有办法确定它里面内容的大小,所以只能确定不让子View超过父容器的大小。resultSize = size,resultMode为MeasureSpec.AT_MOST。
- MeasureSpec.AT_MOST:父容器提供一个最大值。同样是分三种情况:
- childDimension > 0:同样,如果子View设置的固定的值,那么resultSize就为它设定的值。resultMode应该就是精确的。
- childDimension == LayoutParams.MATCH_PARENT:如果是想MATCH_PARENT,那就让resultSize等于父容器给的这个最大值。resultMode= MeasureSpec.AT_MOST。
- childDimension == LayoutParams.WRAP_CONTENT:如果是要包裹内容,那么就让resultSize等于父容器能给的最大值,只要让他不超过这个值就可以了。所以resultMode = MeasureSpec.AT_MOST
- MeasureSpec.UNSPECIFIED:为系统多次measure调用的。
回到ViewGroup的measureChildWithMargins方法中,现在获取到了要测量child的childWidthMeasureSpec和childHeightMeasureSpec,继续调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
此时又回到了view的measure方法,所以又执行了里面的onMeasure方法。此时的这个view就是decorview的子View,包含一个
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
先看getSuggestedMinimumWidth方法,代码如下
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
就是如果没有背景,就返回我们设置的最小值,如果有背景,就返回最小值和背景的最大值,也就是提供一个默认的最小值而已,接着调用了getDefaultSize()
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.AT_MOST和 MeasureSpec.EXACTLY时会返回measureSpec给定的值,基本上大多的时候也都会走到这里。将得到的宽高值传入setMeasuredDimension方法中,会调用setMeasuredDimensionRaw,在setMeasuredDimensionRaw中
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
终于对view的mMeasuredWidth 、mMeasuredHeight成员变量完成了赋值,并改变了标记。
此时回到FrameLayout的onMeasure方法中。执行完 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
其实就是对该FrameLayout下的所有child完成了测量,此时就能通过getMeasuredWidth和getMeasuredHeight获取他们测量后的宽高了。执行完onMeasure的for循环后,FrameLayout的onMeasure又执行了
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
来完成对自己mMeasuredWidth、mMeasuredHeight的赋值。至此所有view的宽高全都测量出来了。
补充:在FrameLayout的onMeasure中,for循环中有一行childState = combineMeasuredStates(childState, child.getMeasuredState());
得到一个childState的值,并且在setMeasuredDimension中的resolveSizeAndState中将childState传入其中,
查看View中的三个方法:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredState() {
return (mMeasuredWidth&MEASURED_STATE_MASK)
| ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
public final int getMeasuredHeightAndState() {
return mMeasuredHeight;
}
发现getMeasuredWidth方法返回并不是单纯的mMeasuredWidth ,而是掩码。其实mMeasuredWidth并不是单纯代表着宽度的数值。它的前8位代表着测量状态,它的后24位才代表着具体数值。所以getMeasuredWidth方法要返回具体数值要mMeasuredWidth & MEASURED_SIZE_MASK;
而单纯返回mMeasuredHeight的方法名的意思是返回测量后的高和状态。getMeasuredState把width和height的state分别封装到int中的前8位和16-24位。看下resolveSizeAndState的方法
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
当measureSpec为AT_MOST的时候,也就是对应warp_content的场景,并且父容器提供的最大值小于了该类想要的值时,虽然我们依然给了他measureSpec中的值,但是加入了MEASURED_STATE_TOO_SMALL这个标记,标记测量的时候没有给到他相应的值。
Layout(布局)
所有要摆放物品的大小都已经测量完了,这时候就需要规划把它们具体摆放在哪了。查看ViewRootImpl的performLayout方法,host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
host是DecorView,由于此时DeocrView已经测量完毕,所以已经可以调用getMeasuredWidth、getMeasuredHeight来获取它的测量宽高了。进入View类的layout方法,首先会执行boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
查看setOpticalFrame方法内部也是调用了setFrame方法。在setFrame中定义了一个boolean类型的changed变量,初始值为false。然后判断如果此View原来的left、right、 top 、bottom其中的任何一个和现在传入的四个值不同,就说明此view的布局要有所改变 ,这时将changed变量赋值为true。并将原来的成员变量进行相应的更新。比对原来的尺寸和现在的尺寸是否一样,如果不一样,执行了onSizeChanged()方法。这也是onSizeChanged方法回调的时机。在view的layout方法中,执行完boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
如果changed为true或者mPrivateFlags有需要重新layout的标记,执行onLayout(changed, l, t, r, b);
在view中onLayout是空实现,而onLayout在ViewGroup中的一个抽象方法,由继承的子类必须实现。因为每个具体的view,按什么规则来摆放自己view都会有不同的规则,所以这事view和ViewGroup不可能帮着去做。那就进入到DeocrView的onLayout方法中。它先调用了FrameLayout的onLayout方法,在这个方法中直接执行了layoutChildren(left, top, right, bottom, false /* no force left gravity */);
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 省略一大段代码:根据具体的逻辑来计算child应该摆放的位置值
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
此时又调用了view的layout方法,又会回到上面的逻辑,进入一个深度遍历,如果是ViewGroup,继续执行它的child的layout,直到全部view都执行完layout.
Draw(绘制)
所有的view都已经规划完了需要放在哪里,这时候就要把每个view都显示在他们需要显示的位置上。draw的起始点是在ViewRootImpl的performDraw方法中,执行了draw(fullRedrawNeeded);
关键代码如下:
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
} else {
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
只留了两句最关键的代码,通过判断是否开启了硬件加速渲染,分别执行了mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
和 drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)
,在这两个方法中最终都执行了mView的draw(canvas)方法。这次又来到了View的draw方法中。这个方法有很清晰的6个步骤执行顺序。
- 绘制背景
- 如果有必要,保存当前图层
- 绘制View本身内容
- 绘制子view
- 如果有必要,绘制边缘,恢复图层
- 绘制view上装饰性的,比如滚动条
其中2和5是可以跳过的。
绘制背景
执行drawBackground(canvas);
代码很简单,就是将view的background绘制到canvas上。
绘制View本身内容
执行onDraw(canvas);
,onDraw在view中为空实现,具体的实现需要在具体的类中分别实现。因为每个类要绘制的内容都是不一样的
绘制子view
执行了dispatchDraw(canvas);
在view中这个方法是一个空的实现,而在ViewGroup中有了具体的实现。这也很对,因为只有ViewGroup才需要绘制子View,所以才会去具体实现dispatchDraw()方法。在ViewGroup的dispatchDraw中代码很多,但是主要是调用了drawChild(canvas, transientChild, drawingTime);
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
来执行child的draw,进入遍历过程来执行view树的draw方法。
绘制view上装饰性
执行onDrawForeground(canvas);
执行完遍历过程后,view就绘制完成了