一. 前言

1. 作用

Measure过程用来测量View的宽度和高度。

2. 注意

① 在某些情况下,需要多次测量Measure才能确定View的最终宽高

② 因此,在上述情况下,Measure过程后得到的宽高不准确。

③ 因此,建议在Layout过程中onLayout去获取最终的宽高。

二. 储备知识

1. ViewGroup.LayoutParms类

① 作用

布局参数类,指定View的宽度width和高度height等布局参数。

② 具体使用

参数 解释
match_parent 强制性使子视图的大小扩展至与父视图大小相等,不含padding
wrap_content 自适应大小,强制性使视图扩展以便显示其全部内容,含padding
具体的值 dp/px

2. MeasureSpecs类

① 简介

测量规格类,测量View大小的依据。每个MeasureSpecs代表了一组宽高的测量规格。

宽测量规格 widthMeasureSpecs 和 高测量规格 heightMeasureSpecs。

② 组成

测量规格MeasureSpecs = 测量模式Mode + 测量大小Size

③ Mode的类型

模式 具体描述
UNSPECIFIED 父视图不约束子视图View,如ListView,ScrollView等,一般不常用
EXACTLY 父视图为子视图指定一个确切的尺寸,子视图必须在指定的尺寸内。如使用具体数值或者match_parent。
AT_MOST 父视图为子视图指定一个最大尺寸,子视图必须确保自身以及所有子视图可以适应在该尺寸内。

④ 实际使用

1
2
3
4
5
6
7
8
9
10
11
12
/**
* MeasureSpec类的具体使用
**/

// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)

// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)

// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

三. MeasureSpec值的测量

子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的。具体计算逻辑封装在getChildMeasureSpec里。

即:子view的大小由父viewMeasureSpec值 和 子viewLayoutParams属性 共同决定。

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
/**
* 源码分析:getChildMeasureSpec()
* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
**/

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

//参数说明
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)

//父view的测量模式
int specMode = MeasureSpec.getMode(spec);

//父view的大小
int specSize = MeasureSpec.getSize(spec);

//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);

//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;

//通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
// 当父view的模式为EXACITY时,父view强加给子view确切的值
//一般是父view设置为match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子view的LayoutParams>0,即有确切的值
if (childDimension >= 0) {
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;

// 当子view的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;

// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
// 多见于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

结论如下:

规律总结:

四. Measure过程详解

1. 单一View的Measure过程

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
/**
* 源码分析:measure()
* 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
* 作用:基本测量逻辑的判断
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算视图大小 ->> 分析1
} else {
...
}
}

/**
* 分析1:onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View的宽/高值:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
// setMeasuredDimension() :获得View宽/高的测量值 ->> 分析2
// 传入的参数通过getDefaultSize()获得 ->> 分析3
}

/**
* 分析2:setMeasuredDimension()
* 作用:存储测量后的View宽 / 高
* 注:该方法即为我们重写onMeasure()所要实现的最终目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//参数说明:测量后子View的宽 / 高值
// 将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
// 下面我们继续看getDefaultSize()的介绍

/**
* 分析3:getDefaultSize()
* 作用:根据View宽/高的测量规格计算View的宽/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 参数说明:
// size:提供的默认大小
// measureSpec:宽/高的测量规格(含模式 & 测量大小)

// 设置默认大小
int result = size;

// 获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;

// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}

// 返回View的宽/高值
return result;
}

由于UNSPECIFIED模式适用于系统内部多次measure情况,很少用到,所以暂时不讨论UNSPECIFIED模式。

总结过程:

measure -> onMeasure -> setMeasuredDimension + getDefaultSize

2. 多个View,ViewGroup的Measure过程

① 原理

遍历 测量所有子View的尺寸,合并所有子View的尺寸,最终得到ViewGroup父视图的测量值。

② 过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 源码分析:measure()
* 作用:基本测量逻辑的判断;调用onMeasure()
* 注:与单一View measure过程中讲的measure()一致
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {

// 调用onMeasure()计算视图大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...
}

/**
* 分析1:onMeasure()
* 作用:遍历子View & 测量
* 注:ViewGroup = 一个抽象类 = 无重写View的onMeasure(),需自身复写
**/

为什么ViewGroupmeasure过程不像单一Viewmeasure过程那样对onMeasure()做统一的实现?

  • 因为不同的ViewGroup子类(LinearLayoutRelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。
  • 因此,ViewGroup无法对onMeasure()作统一实现。这个也是单一View的measure过程与ViewGroup过程最大的不同。
  • 即 单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有。
  • 其实,在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需更精细的测量。所以有时候也需重写onMeasure()

在自定义ViewGroup中,关键在于:根据需求复写onMeasure()从而实现你的子View测量逻辑。复写onMeasure()的套路如下:

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
/**
* 根据自身的测量逻辑复写onMeasure(),分为3步
* 1. 遍历所有子View & 测量:measureChildren()
* 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值(自身实现)
* 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
**/

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

// 定义存放测量后的View宽/高的变量
int widthMeasure ;
int heightMeasure ;

// 1. 遍历所有子View & 测量(measureChildren())
// ->> 分析1
measureChildren(widthMeasureSpec, heightMeasureSpec);

// 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
void measureCarson{
... // 自身实现
}

// 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
// 类似单一View的过程,此处不作过多描述
setMeasuredDimension(widthMeasure, heightMeasure);
}
// 从上可看出:
// 复写onMeasure()有三步,其中2步直接调用系统方法
// 需自身实现的功能实际仅为步骤2:合并所有子View的尺寸大小

/**
* 分析1:measureChildren()
* 作用:遍历子View & 调用measureChild()进行下一步测量
**/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:父视图的测量规格(MeasureSpec)
final int size = mChildrenCount;
final View[] children = mChildren;

// 遍历所有子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 调用measureChild()进行下一步的测量 ->>分析1
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

/**
* 分析2:measureChild()
* 作用:a. 计算单个子View的MeasureSpec
* b. 测量每个子View最后的宽 / 高:调用子View的measure()
**/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {

// 1. 获取子视图的布局参数
final LayoutParams lp = child.getLayoutParams();

// 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
// getChildMeasureSpec() 请看上面第2节储备知识处
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
mPaddingTop + mPaddingBottom, lp.height);

// 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
// 下面的流程即类似单一View的过程,此处不作过多描述
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 回到调用原处

参考文章

自定义View Measure过程 - 最易懂的自定义View原理系列(2)