深入理解MeasureSpec
前言
上一篇DecorView添加到Window过程的源码分析我们找到了UI绘制流程的起始点,也就是在ViewRootImpl
的performTraversals()
依次执行performMeasure
、performLayout
、performDraw
,那么这个MeasureSpec又是什么呢?它是View的一个内部类,从名字我们可以看出,这是一个测量规格,它决定了View的测量过程。
MeasureSpec
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
*/
* MeasureSpec封装从父对象传递给孩子的布局要求。
* 每个MeasureSpec表示宽度或高度的要求。
* 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)
}
}
}
从MeasureSpec类的定义我们知道,它封装了对子View的布局要求,由尺寸和模式组成,其实MeasureSpec代表一个32位的int值,高2位表示SpecMode,低30位表示SpecSize,而SpecSize是指在某种SpecMode下的规格大小,从源码我们看出它内部定义了很多常量,从api17以后开始采用位运算,因为位运算的效率最高,我们看下三种模式
-
UNSPECIFIED = 0 << MODE_SHIFT:即: 00
000000 00000000 00000000 00000000
父容器不对子View有任何限制,子View要多大给多大,有系统内部调用,我们不需要研究,例如ScrollView -
EXACTLY =1<< MODE_SHIFT:即: 01
000000 00000000 00000000 00000000
父容器已经测量出子View所需要的大小,即measureSpec中封装的specsize,对应于LayoutParams中的match_parent
和设置的固定值 -
AT_MOST =2 << MODE_SHIFT:即: 10
000000 00000000 00000000 00000000
父窗口限定了一个最大值给子View即SpecSize,对应于LayoutParams中的wrap_content
size & ~MODE_MASK
是获得SpecSize,mode & MODE_MASK
是获得SpecMode,之后再或运算即可得到MeasureSpec。看下图具体运算
位运算与非.png
我们在使用View时是直接设置LayoutParams,但是在View测量的时候,系统会将LayoutParams在父容器的约束下进行相对应的MeasureSpec,然后在根据这个MeasureSpec来确定View的测量后的宽高,由此可见,MeasureSpec不是LayoutParams唯一决定的,还需要父容器一起来决定,在进一步决定View的宽高。但是顶级View,也就是上文我们分析到的DecorView和普通的View的MeasureSpec计算有些区别,对于DecorView,其MeasureSpec是由屏幕的尺寸和LayoutParams决定的,而DecorView的默认LayoutParams就是match_parent
(在初始化DecorView时可知),对于普通View来说,其MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams决定。在performTraversals()
方法中有如下一段
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
我们再来看下getRootMeasureSpec
方法的实现
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;
}
通过上述代码,对于DecorView来说就是走第一个case
,对于普通View来说,也就是我们Activity显示布局的根View是一个ViewGroup,我们再来看下ViewGroup的measureChildWithMargins()
方法
ViewGroup.java#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);
}
可以发现在调用子View的measure()
之前会先通过getChildMeasureSpec()
方法来得到子View的MeasureSpec,通过分析,我们明显可以发现子View的MeasureSpec的创建与父容器的MeasureSpec和自身的LayoutParams有关,我们再来看下getChildMeasureSpec()
ViewGroup.java#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);
}
上面代码主要是先获得到父容器的SpecMode,在通过子View自身设置的LayoutParams来进一步决定子View的SpecMode,最后在通过MeasureSpec.makeMeasureSpec(resultSize, resultMode);
返回子View的MeasureSpec然后再去measure()
.getChildMeasureSpec()这个方法很清晰的展示出子View创建MeasureSpec的过程,下面我们通过表格在说明一下
- 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
- 当View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间
- 当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以确定出子元素的MeasureSpec,进一步便可以确定出测量后的大小。
总结
好了,至此为止,MeasureSpec含义模式以及创建规则就基本说完了,下一篇开始我们分析onMeasure()等调用过程和相关方法。
推荐
DecorView添加到Window过程的源码分析
AppCompatActivity的setContentView()源码分析
Activity的setContentView()源码分析