自定义布局
布局过程
- 确定每个 
View的位置和尺⼨ - 作用:为绘制和触摸范围做⽀持
- 绘制:知道往哪⾥绘制
 - 触摸反馈:知道用户点的是哪⾥
 
 
流程
- 从整体看:
- 测量流程:从根 
View递归调用每⼀级子View的measure()⽅法,对它们进⾏测量 - 布局流程:从根 
View递归调用每⼀级子View的layout()⽅法,把测量过程得出的子View的位置和尺⼨传给子View,子View保存 - 为什么要分两个流程?
 
 - 测量流程:从根 
 - 从个体看,对于每个 
View:- 运⾏前,开发者在 
xml⽂件⾥写⼊对View的布局要求layout_xxx - ⽗ 
View在自己的onMeasure()中,根据开发者在xml中写的对子View的要求,和自己的可用空间,得出对子View的具体尺⼨要求 - 子 
View在自己的onMeasure()中,根据自己的特性算出自己的期望尺⼨如果是ViewGroup,还会在这⾥调用每个子View的measure()进⾏测量 - ⽗ 
View在子View计算出期望尺⼨后,得出子View的实际尺⼨和位置 - 子 
View在自己的layout()⽅法中,将⽗View传进来的自己的实际尺⼨和位置保存如果是 ViewGroup,还会在 onLayout() ⾥调用每个字View的 layout() 把它们的尺⼨位置传给它们 
 - 运⾏前,开发者在 
 
具体开发
继承已有的
View,简单改写它们的尺⼨:重写onMeasure():SquareImageView重写
onMeasure()用
getMeasuredWidth()和getMeasuredsize()获取到测量出的尺⼨计算出最终要的尺⼨
用
setMeasuredDimension(width, height)把结果保存1
2
3
4
5
6
7protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int `size` = Math.min(width, height);
setMeasuredDimension(`size`, `size`);
}
对自定义
View完全进⾏自定义尺⼨计算:重写onMeasure():CircleView重写
onMeasure()计算出自己的尺⼨
用
resolvesize()或者resolvesizeAndState()修正结果resolvesize()/resolvesizeAndState()内部实现(⼀定读⼀下代码,这个极少需要自己写,但⾯试时很多时候会考):- ⾸先用 
MeasureSpec.getMode(measureSpec)和MeasureSpec.getsize(measureSpec)取出⽗ 对自己的尺⼨限制类型和具体限制尺⼨; - 如果 
measure spec的mode是EXACTLY,表示⽗View对子View的尺⼨做出了精确限制,所以就放弃计算出的size,直接选用measure spec的size; - 如果 
measure spec的mode是AT_MOST,表示⽗View对子View的尺⼨只限制了上限,需要看情况:- 如果计算出的 
size不⼤于spec中限制的size,表示尺⼨没有超出限制,所以选用计算出的size; - ⽽如果计算出的 
size⼤于spec中限制的size,表示尺⼨超限了,所以选用spec 的size,并且在 resolvesizeAndState() 中会添加标志MEASURED_STATE_TOO_SMALL(这个标志可以辅助⽗View做测量和布局的计算; 
 - 如果计算出的 
 - 如果 
measure spec的mode是UNSPECIFIED,表示⽗View对子View没有任何尺⼨限制,所以直接选用计算出的size,忽略spec中的size。 
- ⾸先用 
 
使用
setMeasuredDimension(width, height)保存结果1
2
3
4
5
6
7protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = (int) ((PADDING + RADIUS) * 2);
int height = (int) ((PADDING + RADIUS) * 2);
setMeasuredDimension(resolve`size`AndState(width, widthMeasureSpec,
0),
resolve`size`AndState(height, heightMeasureSpec, 0));
}
自定义
Layout:重写onMeasure()和onLayout():TagLayout重写
onMeasure()遍历每个子
View,用measureChildWidthMargins()测量子View需要重写
generateLayoutParams()并返回MarginLayoutParams才能使用measureChildWithMargins()⽅法有些子
View可能需要重新测量(⽐如换⾏处)测量完成后,得出子
View的实际位置和尺⼨,并暂时保存1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23protected void onMeasure(int widthMeasureSpec, int
heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
`View`child = getChildAt(i);
Rect childBounds = childrenBounds[i];
// 测量子 View
measureChildWithMargins(child, widthMeasureSpec,
widthUsed,
heightMeasureSpec, heightUsed);
// 保存子 `View`的位置和尺⼨
childBounds.set(childlLeft, childTop, childLeft
+ child.getMeasuredWidth(), chiltTop
+ child.getMeasuredHeight());
......
}
// 计算自己的尺⼨,并保存
int width = ...;
int height = ...;
setMeasuredDimension(resolve`size`AndState(width,
widthMeasureSpec, 0),
resolve`size`AndState(height, heightMeasureSpec,
0));
}measureChildWidthMargins()的内部实现(最好读⼀下代码,这个极少需要自己写,但⾯试时很多时候会考):通过
getChildMeasureSpec(int spec, int padding, int childDimension)⽅法计算出子View的widthMeasureSpec和heightMeasureSpec,然后调用child.measure()⽅法来让子View自我测量;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// ViewGroup.measureChildWithMargins() 源码
protected void measureChildWithMargins(`View`child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp =
(MarginLayoutParams) child.getLayoutParams();
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);
child.measure(childWidthMeasureSpec,
childHeightMeasureSpec);
}getChildMeasureSpec(int spec, int padding, int childDimension)⽅法的内部实现是,结合开发者设置的LayoutParams中的width和height与⽗View自己的剩余可用空间,综合得出子View的尺⼨限制,并使用MeasureSpec.makeMeasureSpec(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
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// ViewGroup.getChildMeasureSpec() 源码
public static int getChildMeasureSpec(int spec, int
padding,
int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int spec`size` = MeasureSpec.get`size`(spec);
int `size` = Math.max(0, spec`size` - padding);
int result`size` = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact `size` on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
result`size` = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==
LayoutParams.MATCH_PARENT) {
// Child wants to be our `size`. So be it.
result`size` = `size`;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==
LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own `size`. It
can't be
// bigger than us.
result`size` = `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
result`size` = 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.
result`size` = `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.
result`size` = `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
result`size` = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension ==
LayoutParams.MATCH_PARENT) {
// Child wants to be our `size`... find out how
big it should
// be
result`size` =
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
result`size` =
View.sUseZeroUnspecifiedMeasureSpec ? 0 : `size`;
resultMode = MeasureSpec.UNSPECIFIED; }
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(result`size`,
resultMode);
}注意:源码中的分类⽅式是先⽐较自己的
MeasureSpec中的mode,再⽐较开发者设置的
layout_width和layout_height,⽽我给出的判断⽅式(下⾯的这⼏段内容)是先⽐较
layout_width和layout_height,再⽐较自己
MeasureSpec中的mode。两种分类⽅法都能得出正确的结果,但源码中的分类⽅法在逻辑上可能不够直观,如果你读源码理解困难,可以
尝试用我上⾯的这种⽅法来理解。
- 如果开发者写了具体值(例如 
layout_width="24dp"),就不用再考虑⽗View的剩余空间了,直接用LayoutParams.width / height来作为子View的限制size,⽽限制mode为EXACTLY(为什么?课堂上说过,因为软件的直接开发者——即xml布局⽂件的编写者——的意⻅最重要,发⽣冲突的时候应该以开发者的意⻅为准。换个⻆度说,如果真的由于冲突导致界⾯不正确,开发者可以通过修改xml⽂件来解决啊,所以开发者的意⻅是第⼀位,但你如果设计成冲突时开发者的意⻅不在第⼀位,就会导致软件的可配置性严重降低); - 如果开发者写的是 
MATCH_PARENT,即要求填满⽗控件的可用空间,那么由于自己的可用空间和自己的两个MeasureSpec有关,所以需要根据自己的widthMeasureSpec或heightMeasureSpec中的mode来分情况判断: - 如果自己的 
spec中的mode是EXACTLY或者AT_MOST,说明自己的尺⼨有上限,那么把spec中的size减去自己的已用宽度或⾼度,就是自己可以给子View的size;⾄于mode,就用EXACTLY(注意:就算自己的mode是AT_MOST,传给子View的也是EXACTLY,想不通的话好好琢磨⼀下); - 如果自己的 
spec中的mode是UNSPECIFIED,说明自己的尺⼨没有上限,那么让子View填满自己的可用空间就⽆从说起,因此选用退让⽅案:给子View限制的mode就设置为UNSPECIFIED,size写 0 就好; - 如果开发者写的是 
WRAP_CONTENT,即要求子View在不超限制的前提下自我测量,那么同样由于自己的可用空间和自己的两个MeasureSpec有关,所以也需要根据自己的widthMeasureSpec和heightMeasureSpec中的mode来分情况判断: - 如果自己的 
spec中的mode是EXACTLY或者AT_MOST,说明自己的尺⼨有上限,那么把spec中的size减去自己的已用宽度或⾼度,就是自己可以给子View的尺⼨上限;⾄于mode,就用AT_MOST(注意,就算自己的mode是EXACTLY,传给子View的也是AT_MOST,想不通的话好好琢磨⼀下; - 如果自己的 
spec中的mode是UNSPECIFIED,说明自己的尺⼨没有上限,那么也就不必限制子View的上限,因此给子View限制的mode就设置为UNSPECIFIED,size写 0 就好。 
- 如果开发者写了具体值(例如 
 
测量出所有子
View的位置和尺⼨后,计算出自己的尺⼨,并用setMeasuredDimension(width, height)保存
重写
onLayout()遍历每个子
View,调用它们的layout()⽅法来将位置和尺⼨传给它们1
2
3
4
5
6
7
8
9
10protected void onLayout(boolean changed, int l, int t, int r,
int b) {
for (int i = 0; i < getChildCount(); i++) {
`View`child = getChildAt(i);
Rect childBounds = childrenBounds[i];
// 将每个子 `View`的位置和尺⼨传递给它
child.layout(childBounds.left, childBounds.top,
childBounds.right, childBounds.bottom);
}
}