Android 中 view 的测量机制

MeasureSpec

MeasureSpec 使用一个 32位的 int 数值用来存放 mode 和 size

前2位存放 mode ,后30位存放 size

其中,mode 的类型一共有三种,分别是

mode 解释
UNSPECIFIED 0 << 30(即00,000..[一共30个0]..000) 未指定模式,不对子 view 的尺寸进行限制
EXACTLY 1 << 30(即01,000..[一共30个0]..000) 精确模式,子 view 的尺寸为具体的数值(xxdp)或者父 view 的大小(match_parent)
AT_MOST 2 << 30(即10,000..[一共30个0]..000) 最大模式,子 view 的尺寸可以在指定范围内要多大就多大(wrap_content)

UNSPECIFIEDAT_MOST 的区别在于 UNSPECIFIED 不限制 子 view 的大小

同时,MeasureSpec 提供了两个方法,用来获取 modesize

PS.

private static final int MODE_MASK = 0x3 << 30;
即11,000..[一共30个0]..000

获取 mode

android.view.View.MeasureSpec#getMode

通过「&」方法保留前两位数,将后30位变为0

1
2
3
4
5
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}

获取 size

先将 MODE_MASK 按位取反,得到
00,111…[一共30个1]…111
再通过「&」方法保留后30位数,将前两位变为0,则获得 size
android.view.View.MeasureSpec#getSize

1
2
3
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

View 的测量

onMeasure

先来看 android.view.View#onMeasure 方法

通过 getDefaultSize 方法获取 view 默认的宽/高后进行设置

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 方法是获取该 view 的最小宽度/高度

1
2
3
4
5
6
7
8

/**
* 如果没有背景,则返回设置的最小宽度(minWidth)
* 否则返回背景最小宽度和设置的最小宽度(minWidth)的大者
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

接着看 getDefaultSize 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @param size view 的默认大小
* @param measureSpec 父 view 对子 view 的约束
* @return 返回大小
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
//父 view 对子 view 不做限制,则使用默认的大小
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//父 view 对子 view 限制,则使用特定的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

那么问题来了,调用子 view 的 onMeasure() 方法里的参数是怎么来的呢,我们回到 ViewGroup.measureChild() 方法

在这个方法中,会调用子 view 的 measure 方法,在 measure 方法中会调用 onMeasure 方法去测量自己的宽/高,也就是上面讲过的 android.view.View#onMeasure 方法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param child 子 view
* @param parentWidthMeasureSpec 父 view 宽的 MeasureSpec
* @param parentHeightMeasureSpec 父 view 高的 MeasureSpec
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可见,在这个方法中,根据parentWidthMeasureSpec、padding 以及子 view 的 LayoutParams 的设置,通过 getChildMeasureSpec() 该方法确定了子 view的 MeasureSpec,后再调用 child.measure 方法对子 view 进行测量

下来重点看一下 getChildMeasureSpec() 这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
* @param spec 父 view 的 MeasureSpec
* @param padding 父 view 设置的 padding
* @param childDimension 该 view 希望设置的大小(即 LayoutParam 的 width/height or xml 中设置的 layout_width/layout_height)
* @return 返回该 view 的 MeasureSpec
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父 view 的 mode
int specMode = MeasureSpec.getMode(spec);
//获取父 view 的 size
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
//父 view 的大小是固定的
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果子 view 设置了大小,则使用子 view 自己设置的大小和,mode 为 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子 view 设置为 MATCH_PARENT,则使用父 view 的大小,mode 为 EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子 view 要决定自己的大小(设置为 WRAP_CONTENT),不能超过父 view,则使用父 view 的大小,mode 为 AT_MOST(最大不能超过 size)
// 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
//父 view 没有固定大小,但有上限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
//如果子 view 设置了大小,则使用子 view 自己设置的大小,mode 为 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view 要求跟父 view 一样大,但父 view 是不固定的
//只能约束子 view 不超过父 view,则使用父 view 的大小,mode 为 AT_MOST(最大不能超过 size)
// 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) {
//子 view 要决定自己的大小(设置为 WRAP_CONTENT),不能超过父 view,则使用父 view 的大小,mode 为 AT_MOST(最大不能超过 size)
// 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
//父 view 没有限制,可以为任何大小
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子 view 设置了大小,则使用子 view 自己设置的大小,mode 为 EXACTLY
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view 要求跟父 view 一样大,则继续看看子 view应该多大
//在 Api 23 以下设置 size 为 0,Api 23以上为父 view 的 size
//mode 设置为 UNSPECIFIED
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子 view 要决定自己的大小(设置为 WRAP_CONTENT),
//在 Api 23 以下设置 size 为 0,Api 23以上为父 view 的 size
//mode 设置为 UNSPECIFIED
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
//最后将 size 和 mode 组装并返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

该方法是通过父 view子 view的限制来进行计算子 view应有的 size 和 mode

也可以通过另一种角度来确定子 view的 size 和 mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec), specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0, resultMode = 0;
if (childDimension >= 0) {
// 子元素指定了具体的大小,就用子元素的大小,mode 使用 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
// 子元素希望和父控件一样大,需要设置其上限,然后测量模式与父控件一致即可
if (specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST) {
resultSize = size;
resultMode = specMode;
} else if (specMode == MeasureSpec.UNSPECIFIED) {
// API23以下就是0,父控件没有指定大小的时候,子控件只能是0;以上是size
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 子元素希望自己决定大小,设置其大小的上限是父控件的大小即可
if (specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (specMode == MeasureSpec.UNSPECIFIED) {
// API23以下就是0,父控件没有指定大小的时候,子控件只能是0;以上是size
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

作者:HelloDev
链接:https://juejin.cn/post/6844903694073331720
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。