MeasureSpec
在开始进行理解View的测量流程之前,需要先理解MeasureSpec。
MeasureSpec代表的是32位的int值,它的高2位是SpecMode(也是一个int),低30位是SpecSize(也是一个int),SpecMode是测量模式,SpecSize是测量大小。
MeasureSpec相当于是两者的结合。系统封装了如何从MeasureSpec中提取SpecMode和SpecSize,也封装了用SpecMode和SpecSize组合成MeasureSpec。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
SpecMode有3种,分别是
-
UNSPECIFIED 父容器对View不会有任何限制,要多大给多大,一般是用在系统内部使用,我们开发的APP用不到
-
EXACTLY 这种情况对应于match_parent和具体数值这两种模式,父容器已经检测出View需要的精确大小
-
AT_MOST 这种情况对应于wrap_content,父容器指定了一个最大值,View不能超过这个值
一般来说,View的MeasureSpec由父容器的MeasureSpec和自己的LayoutParams共同决定。因为有了MeasureSpec才可以在onMeasure中确定测量宽高。
下面是从源码中提取出来的摘要信息,后面会详细看源码分析,这里先提取出来
-
如果View的宽高是固定的值,那么不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY
-
如果View的宽高是wrap_content,那么不管父容器的MeasureSpec是EXACTLY还是AT_MOST,最终View的MeasureSpec都是AT_MOST,这里暂时不用管UNSPECIFIED(我们用不到)。而且View最终的大小不能超过父容器的剩余空间
-
如果View的宽高是match_parent,那么要分两种情况
-
如果父容器是EXACTLY,那么View就是EXACTLY
-
如果父容器是AT_MOST,那么View也是AT_MOST
-
这里不得不引用一张刚哥书籍里面的经典表格来表示一下。
从performMeasure()开始View测量
我们从ViewRootImpl的performTraversals()开始着手,仔细观察
private void performTraversals() {
//host为根视图,即DecorView
//desiredWindowWidth是Window的宽度
measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
//分析1 : desiredWindowWidth就是Window的宽度,desiredWindowHeight是Window的高度
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//分析2 : 开始测量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measure是从上往下执行的,widthMeasureSpec和heightMeasureSpec通常情况下是由父容器传递给子视图的。但是最外层的根视图,怎么拿到MeasureSpec呢?在执行performMeasure方法之前,我们需要拿到最外层的视图的MeasureSpec,看代码
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//如果是MATCH_PARENT,那么就是EXACTLY
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//如果是WRAP_CONTENT,就是AT_MOST
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
//如果是固定的值,也是EXACTLY
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
最外层的根视图的MeasureSpec只由自己的LayoutParams决定,做自己的主人,舒服。
既然我们根视图拿到了MeasureSpec,接下来就要拿自己的MeasureSpec教孩子做人了。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//如果是MATCH_PARENT,那么就是EXACTLY
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//如果是WRAP_CONTENT,就是AT_MOST
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
//如果是固定的值,也是EXACTLY
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
measure方法里面有一些检测是否需要重新onMeasure的代码,被我略去了。
onMeasure是View里面的方法,ViewGroup是一个抽象类并且没有重写onMeasure。因为onMeasure方法的实现,每个都是不一样的,比如LinearLayout和FrameLayout的onMeasure方法肯定是实现逻辑不一样的。
因为DecorView是FrameLayout,所以我们看看FrameLayout中的onMeasure.
FrameLayout->onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//分析1 : 遍历所有子控件,测量每个子控件的大小
//参数1:View控件
//参数2:宽MeasureSpec
//参数3:父容器在宽度上已经用了多少了,因为FrameLayout的规则是:前面已经放置的View并不会影响后面放置View的宽高,是直接覆盖到上一个View上的.所以这里传0
//参数4:高MeasureSpec
//参数5:父容器在高度上已经用了多少了
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
......
//分析2 : 测量完所有的子控件的大小之后,才知道自己的大小 这很符合FrameLayout的规则嘛
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
......
}
FrameLayout的onMeasure方法中会遍历所有子控件,然后进行所有子控件的大小测量。最后才来设置自己的大小。注意,onMeasure方法的入参MeasureSpec是从父容器传过来的,意思就是给你个参考,你自己看着办吧。
在测量子控件大小的时候会调用ViewGroup的measureChildWithMargins方法,下面是代码:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子控件的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//分析1: 计算子控件在宽上的MeasureSpec
//参数1:父容器的MeasureSpec
//参数2:这里官方的入参名称是padding,从下面这个传值的形式来看,显然是子控件在宽上不能利用的空间(ViewGroup的左右两边padding+子控件的左右margin+父容器在宽度上已经使用了并且不能再使用的空间)
//参数3:子控件想要的宽度
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);
//分析2: 将measure过程传递给子控件 如果子控件又是一个ViewGroup,那么继续向下传递
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在measureChildWithMargins方法里我们首先是看到根据子控件的LayoutParams和父容器的MeasureSpec计算子控件的MeasureSpec,然后将计算出的MeasureSpec通过子控件的measure方法传递下去。如果子控件又是一个ViewGroup,那么它又会重复的measure流程,一直向下传递这个过程,直接最后的那个是View为止。因为View没有子控件,它就不能向下传递了。
所以我们自定义View(这里指那种直接继承自View)的时候,在onMeasure方法里面,需要根据自身的LayoutParams+父容器的MeasureSpec来计算SpecSize和SpecMode,最后根据业务场景来确定自己的大小(调用setMeasuredDimension来确定大小)。
注意了,接下来的getChildMeasureSpec方法就比较重要了。
这段代码对应着下面这段总结
-
如果View的宽高是固定的值,那么不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY
-
如果View的宽高是wrap_content,那么不管父容器的MeasureSpec是EXACTLY还是AT_MOST,最终View的MeasureSpec都是AT_MOST,这里暂时不用管UNSPECIFIED(我们用不到)。而且View最终的大小不能超过父容器的剩余空间
-
如果View的宽高是match_parent,那么要分两种情况
-
如果父容器是EXACTLY,那么View就是EXACTLY
-
如果父容器是AT_MOST,那么View也是AT_MOST
-
这段代码对应着上面刚哥总结的那个表格。同时也是measure流程的核心内容。对应刚哥的表格
因为在measureChildWithMargins方法里我们已经计算出子控件的MeasureSpec,然后通过measure传递给子控件了,如果子控件又是一个ViewGroup,那么它又会重复的measure流程,一直向下传递这个过程,直接最后的那个是View为止。因为View没有子控件,它就不能向下传递了。到这里其实我们的View的measure流程已经走完了,哈哈,不知不觉。
下面简单画一个流程图,方便理解上面的流程。
measure小结
从ViewRootImpl的performTraversals方法开始进入View的绘制过程,performTraversals方法里面会有一个performMeasure方法。这个performMeasure方法是专门拿来测量View的大小的。而且会遍历整个View树,全部进行测量。
在performMeasure里面会调用measure方法,然后measure会调用onMeasure方法,而在onMeasure方法中则会对所有的子元素进行measure过程。这相当于完成了一次从父元素到子元素的measure传递过程,如果子元素是一个ViewGroup,那么继续向下传递,直到所有的View都已测量完成。测量完成之后,我们可以根据getMeasureHeight和getMeasureWidth方法获取该View的高度和宽度。