自定义布局
布局过程
- 确定每个
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()
和getMeasured
size()
获取到测量出的尺⼨计算出最终要的尺⼨
用
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()
计算出自己的尺⼨
用
resolve
size()
或者resolve
sizeAndState()
修正结果resolve
size()
/resolve
sizeAndState()
内部实现(⼀定读⼀下代码,这个极少需要自己写,但⾯试时很多时候会考):- ⾸先用
MeasureSpec.getMode(measureSpec)
和MeasureSpec.get
size(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
,并且在 resolvesize
AndState() 中会添加标志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);
}
}