深入理解MeasureSpec
在View的绘制流程一文中提及到MeasureSpec,但是并没有涉及太多,只是一带而过。MeasureSpec是View测量过程中重要的测量依据,这一篇中阅读MeasureSpec源码理解其作用。
1、MeasureSpec介绍
MeasureSpec完整的名称是measure specification,直译为中文叫测量规格。
1.1、测量模式掩码
在了解View的三种测量模式之前,先弄明白MeasureSpec中两个常量的含义:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
MeasureSpec中定义了一个静态常量MODE_MASK,用来从 int 值中提取出测量模式:mode。十六进制0x3,也就是0011,左移30位。使用位移操作因为位移计算在计算机中是非常高效的。
后面计算还会用到MODE_MASK的反码,用来提取出int中存储的View大小:size。
1.2、三种测量模式
一个View的MeasureSpec由大小和模式组成,View共有三种可能的模式:
①UNSPECIFIED:未确定模式。父View没有对子view施加任何约束,子view可以是任何大小。
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
②EXACTLY:精确模式。父View已经为子View确定了确切的尺寸, 不管子View要多大,都会给子View这个值。
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
③AT_MOST:最大模式。子View可以根据需要的大小而定,最大可以达到MeasureSpec中指定的size大小。
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
1.3、构造MeasureSpec
MeasureSpec提供了组装MeasureSpec值的静态方法makeMeasureSpec(int size,int mode),传入测量模式mode和View的大小size,通过该方法将mode和size包装在一个int中。
之所以这样做,最主要的原因还是节省内存,避免内存的频繁分配消耗。因为View的测量过程是及其频繁和复杂的,只用一个int使用非常少的内存即可保存和传递View的大小信息。
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
//早期API ≤ 17
return size + mode;
} else {
//高2位保存mode,低30位保存size
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
对于早期应用内部会通过makeSafeMeasureSpec(int size, int mode)方法计算MeasureSpec。
/**
* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
* will automatically get a size of 0. Older apps expect this.
*
* @hide 仅供内部使用,以与系统小部件和较旧的应用程序兼容
*/
@UnsupportedAppUsage
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
至于多早呢,从View的源码中可以找到答案:在API 17及以前的View如果传入的mode为UNSPECIFIED,那么View的大小直接默认为0。
// Older apps may need this compatibility hack for measurement.
sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
1.4、提取size和mode
MeasureSpec提供了getMode(int measureSpec)方法从包含size和mode的int值中提取出View的测量模式mode。
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
同样,对int低30位用反码即可获得保存的View的size值。
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
最后,MeasureSpec中还有一个adjust(int measureSpec, int delta)方法,用来对MeasureSpec值进行校准,防止size的大小为负。
static int adjust(int measureSpec, int delta) {
//先提取出mode和size
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
//如果mode是未指定,则不校准
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
//将size大小加上校准值delta,但是不能为负,最小也只能为0.
size += delta;
if (size < 0) {
size = 0;
}
return makeMeasureSpec(size, mode);
}
2、MeasureSpec的初始化
View中的MeasureSpec是如何初始化的呢?回顾一下View的绘制流程一文中1.3节的内容。
2.1、根View的MeasureSpec初始化
在ViewRootImpl的performTraversals()方法中开启View的绘制流程:measure -> layout -> draw。只关注测量相关的代码:
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
通过getRootMeasureSpec(int windowSize, int rootDimension)方法获取根MeasureSpec的值:
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;
}
ViewRootImpl中的mWindowAttributes成员变量默认即是WindowManager.LayoutParams类型,这也是根View所使用的属性。
public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
而WindowManager.LayoutParams的构造方法,默认Window的宽高都是MATCH_PARENT:
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
回到getRootMeasureSpec(int windowSize, int rootDimension)方法中,走第一个case:
...
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY)
...
所以根View的大小就是Window窗口的大小,而且是精确值。
2.2、子View的MeasureSpec初始化
以最常见的容器FrameLayout的onMeasure()方法为例,从根View或者其它View层层传递过来带有mode和size的MeasureSpec值:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
...
}
在测量子View之前,先遍历一遍所有子View:通过ViewGroup的measureChildWithMargins(...)方法预先得到子View的MeasureSpec。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//计算子View宽、高的MeasureSpec
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);
//再测量子View
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
最后通过ViewGroup中的getChildMeasureSpec(spec, padding, childDimension)方法计算出子View的MeasureSpec。完整方法如下:
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);
}
上面六十多行代码主要是是case、if判断,逻辑简化成下表:
至此,把View测量中的一个重要概念MeasureSpec搞清楚了。
记得以前开发的时候用ScrollView嵌套ListView,列表始终显示不全。当时刚入门,不知道怎么搞(〃'▽'〃)。其实是因为MeasureSpec为UNSPECIFIED,无法确定列表具体高度导致。
相关资料:
Android Developers > Docs > MeasureSpec
安卓开发之ScrollView嵌套ListView的一些问题和解决