在自定义 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 容器内。请注意,我使用了正确的属性名称和大小值来设置视图的布局参数。