在自定义 View 的学习过程中,不管怎么样都绕不过 MeasureSpec 的学习。拖拖拉拉很久,在数不清的看了忘,忘了看之后,还是决定写篇博客记录一下,毕竟有效的输出才是检验输入的不二法门。废话不多说,下面进入正题。
MeasureSpec 定义
关于 MeasureSpec 的定义,官方解释如下:
A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height.
modesize
MeasureSpec 有三种模式:UNSPECIFIED、EXACTLY、AT_MOST。
- UNSPECIFIED:父布局不对子布局做任何限制,它想多大就多大;一般自定义 View 中用不到;常见于系统内部控件,例如 ListView、ScrollView。
- EXACTLY:父布局对子布局的宽高大小有明确的要求,不管子布局想要多大,它都不能超过父布局对它的限制;指定的大小如 100dp,或者 match_parent(实质上就是屏幕大小),都是确切的尺寸。
- AT_MOST:子布局想要多大就可以多大,但是一般来说不会超过父布局的尺寸;一般对应的父布局尺寸为 wrap_content,父布局无法确定子布局的尺寸。
为了节约内存占用,MeasureSpec 本身就是一个 32 位的 int 值,这个类就是负责将 <size, mode> 的元组转换为 int 值,高 2 位表示 specMode,低 30 位表示 specSize。一个 View 的大小并不是由它自己确定的,而是由其自身的 LayoutParams 以及父布局的 MeasureSpec 确定的。
那 MeasureSpec 是什么,最初的 MeasureSpec 又是哪里来的?
MeasureSpec 缘起
由于 View 的绘制流程入口在 ViewRootImpl 类中,我们最终在 performTraversals 方法中找到如下代码:
```java
public void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
mPrivateFlags = mViewFlags;
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags &= ~PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DIRTY;
mPrivateFlags &= ~PFLAG_DRAWN;
mPrivateFlags &= ~PFLAG_HAS_BOUNDS;
mPrivateFlags &= ~PFLAG_WATCH_OUTSIDE_TOUCH;
mPrivateFlags &= ~PFLAG_LAYOUT_DIRECTION;
mPrivateFlags &= ~PFLAG_LAYOUT_NO_LIMITS;
mPrivateFlags &= ~PFLAG_LAYOUT_NOT_SET;
mPrivateFlags &= ~PFLAG_IS_ROOT_NAMESPACE;
mPrivateFlags &= ~PFLAG_IS_LAZY;
mPrivateFlags &= ~PFLAG_IS_CLEAN;
mPrivateFlags &= ~PFLAG_IS_REFLECTED;
mPrivateFlags &= ~PFLAG_IS_HIDDEN;
mPrivateFlags &= ~PFLAG_IS_INDETERMINATE;
mPrivateFlags &= ~PFLAG_ADDITIVE;
mPrivateFlags &= ~PFLAG_TRANSLUCENT;
mPrivateFlags &= ~PFLAG_SELECTED;
mPrivateFlags &= ~PFLAG_CHECKABLE;
mPrivateFlags &= ~PFLAG_NOT_FOCUSABLE;
mPrivateFlags &= ~PFLAG_NOT_TOUCHABLE;
mPrivateFlags &= ~PFLAG_NOT_VISIBLE;
mPrivateFlags &= ~PFLAG_IMPORTANTForAccessibility;
mPrivateFlags &= ~PFLAG_IMPORTANTForAutofill;
mPrivateFlags &= ~PFLAG_IMPORTANTForContentDescriptions;
mPrivateFlags &= ~PFLAG_IMPORTANTForAccessibilityHidden;
mPrivateFlags &= ~PFLAG_IMPORTANTForAutofillHidden;
mPrivateFlags &= ~PFLAG_IMPORTANTForContentDescriptionsHidden;
}
```
这段代码是关于Android布局测量的。首先,我们需要理解每个函数的作用:
1. `getRootMeasureSpec(mWidth, lp.width)`:这个函数用于获取根视图的宽度测量规格。它接收两个参数:`mWidth`表示当前视图的宽度,`lp.width`表示LayoutParams中的宽度值。
2. `getRootMeasureSpec(mHeight, lp.height)`:这个函数用于获取根视图的高度测量规格。它接收两个参数:`mHeight`表示当前视图的高度,`lp.height`表示LayoutParams中的高度值。
3. `performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)`:这个函数用于执行测量操作。它接收两个参数:`childWidthMeasureSpec`和`childHeightMeasureSpec`,分别表示子视图的宽度测量规格和高度测量规格。
现在我们来重构这段代码:
```java
int rootWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int rootHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(rootWidthMeasureSpec, rootHeightMeasureSpec);
```
重构后的代码与原代码功能相同,但将变量名从`childWidthMeasureSpec`和`childHeightMeasureSpec`更改为`rootWidthMeasureSpec`和`rootHeightMeasureSpec`,以更清楚地表明它们分别表示根视图的宽度和高度测量规格。
以下是重构后的内容:
```java
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;
}
```
其中,`MeasureSpec` 是由 `SpecMode` 和 `SpecSize` 组成的。`MeasureSpec` 通过将 `SpecMode` 和 `SpecSize` 打包成一个整数值来避免过多的对象创建,并提供了对应的打包、解包方法:`MeasureSpec.makeMeasureSpec()` 用于创建一个新的 `MeasureSpec`,而 `MeasureSpec#unwrap()` 则用于解包一个 `MeasureSpec`。
现在我们得到了 MeasureSpec,接下来我们来看看父布局是如何通过 MeasureSpec 来支配子布局的。以下代码截取自 LinearLayout 的 measureVertical 方法:
```java
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
// 二进制的 + ,不是十进制
// 使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
// 例如size=100(就是十进制的 4),mode=AT_MOST,measureSpec=100+1000...00=1000..00100
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
// MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
// 原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
// 原理:同上,将 MASK 取反,得到 00 1111111111(00后跟30个1)
// 将 32,31 替换成 0 也就是去掉了 mode,只保留后30位的size
return (measureSpec & ~MODE_MASK);
}
```
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
$final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(0, childWidth), MeasureSpec.EXACTLY);
$final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin, lp.height);
// 传到各个子 View 的 MeasureSpec 就是在这里生成的
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
```java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父布局的测量模式以及测量大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 记录除去 padding 后的测量大小
int size = Math.max(0, specSize - padding);
// 当前子布局的高度信息和宽度信息
int resultSize = 0;
int resultMode = 0;
// 根据父布局的测量规格进行判断
switch (specMode) {
case MeasureSpec.EXACTLY:
// 如果子布局的高度信息是确定值,说明用户声明了固定的大小,设置子布局的宽高信息为固定值,测量模式为 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==ViewGroup.LayoutParams.MATCH_PARENT) {
// 如果子布局想要填充父布局,设置其大小为父布局的大小,测量模式为 EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==ViewGroup.LayoutParams.WRAP_CONTENT) {
// 如果子布局想自己决定大小,但不能超过父布局的大小,设置其大小为父布局的大小减去 padding,测量模式为 AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
// 如果子布局的高度信息是确定值,设置其大小为固定值,测量模式为 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==ViewGroup.LayoutParams.MATCH_PARENT) {
// 如果子布局想要填充父布局,设置其大小为父布局的大小,测量模式为 AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension ==ViewGroup.LayoutParams.WRAP_CONTENT) {
// 如果子布局想自己决定大小,但不能超过父布局的大小,设置其大小为父布局的大小减去 padding,测量模式为 AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
// 如果子布局的高度信息是确定值,设置其大小为固定值,测量模式为 EXACTLY 或者 UNSPECIFIED
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==ViewGroup.LayoutParams.MATCH_PARENT) {
// 如果子布局想要填充父布局,查找其合适的大小并设置,测量模式为 UNSPECIFIED 或者 EXACTLY
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension ==ViewGroup.LayoutParams.WRAP_CONTENT) {
// 如果子布局想自己决定大小,查找其合适的大小并设置,测量模式为 UNSPECIFIED 或者 EXACTLY
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 用父布局的 MeasureSpec 和子布局的尺寸信息生成自己的测量规格并返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
```
做个小总结:
在这个问题中,我们讨论了View的大小是由它的父布局和它自身共同决定的。为了更好地理解这个概念,我们通过以下几个例子进行了说明:
1. 当测量模式为MeasureSpec.UNSPECIFIED时,最终的高度是已测量的高度加上padding。
```java
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
}
```
2. 当父布局为EXACTLY时,View的大小将严格遵循其指定的宽度和高度。
```xml
android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> android:layout_width="300dp" android:layout_height="300dp" />
```
通过这些例子,我们可以更好地理解View的大小是如何由其父布局和自身共同决定的。
```xml
android:layout_width="MATCH_PARENT" android:layout_height="MATCH_PARENT" android:orientation="VERTICAL"> android:layout_width="MATCH_PARENT" android:layout_height="MATCH_PARENT"/> android:layout_width="MATCH_PARENT" android:layout_height="MATCH_PARENT" android:orientation="VERTICAL"> android:layout_width="WRAP_CONTENT" android:layout_height="WRAP_CONTENT"/> ``` 以下是重构后的代码: ```xml android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> android:layout_width="match_parent" android:layout_height="match_parent"/> android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> android:layout_width="300dp" android:layout_height="300dp"/> ``` 重构后的代码与原始代码相比,主要是将 `/` 标签替换为了正确的结束标签。 以下是重构后的代码段,其中包含了垂直排列的 MyLinearLayout 和 MyTextView: ```xml android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> android:layout_width="wrap_content" android:layout_height="wrap_content" /> ``` 在这个重构后的代码中,我保留了原始代码的结构,并将两个视图组件放入 MyLinearLayout 容器内。请注意,我使用了正确的属性名称和大小值来设置视图的布局参数。