12.2 精通自定义 View 之封装控件——测量和布局

返回自定义 View 目录

12.2.1 ViewGroup 绘制流程

View 和 ViewGroup 的绘制流程基本相同,只是在 ViewGroup 中不仅要绘制自己,还要绘制其中的子控件,而 View 只需要绘制自己就可以了。

绘制流程分为三步:测量、布局、绘制,分别对应 onMeasure()、onLayout()、onDraw() 函数。

  • onMeasure():测量当前控件的大小,为正式布局提供建议(注意:只是建议,至于用不用,要看 onLayout() 函数)。
  • onLayout():使用 layout() 函数对所有字控件进行布局。
  • onDraw():根据布局的位置绘图。

12.2.2 onMeasure() 函数与 MeasureSpec

布局绘画涉及两个过程:测量过程和布局过程。测量过程通过 measure() 函数来实现,是 View 树自顶向下的遍历,每个 View 在循环过程中将尺寸细节往下传递,当测量过程完成以后,所有的 View 都存储了自己的尺寸。布局过程则通过 layout() 函数来实现,也是自顶向下的,在这个过程中,每个父 View 负责通过计算好的尺寸放置它的子 View。

onMeasure() 函数是用来测量当前控件大小的,给 onLayout() 函数提供数值参考。需要特别注意的是,测量完成以后,要通过 setMeasuredDimension(int, int) 函数设置给系统。

1. onMeasure() 函数

1
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

参数 widthMeasureSpec 和 heightMeasureSpec 是父类传递给当前 View 的一个建议值,即想把当前 View 的尺寸设置为宽 widthMeasureSpec、高 heightMeasureSpec。

2. MeasureSpec 的组成

widthMeasureSpec 和 heightMeasureSpec 转换为二进制数字表示,它们都是 32 位的,前 2 位代表模式(mode),后面 30 位代表数值(size)。

1)模式分类
模式 二进制值 含义 对应 XML
UNSPECIFIED 00000000…00000000 父元素不对子元素的确切大小,子元素可以得到任意想要的大小 不常用
EXACTLY 01000000…00000000 父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小 match_parent、具体数值
AT_MOST 10000000…00000000 子元素至多达到指定大小的值 wrap_content
2)模式提取

使用 & 位运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final int MODE_SHIFT = 30;
// 对应:11000000 00000000 00000000 00000000
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 提取模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 提取数值
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

3)MeasureSpec

Android 已经为我们提供了 MeasureSpec 类来实现模式和数值的提取。

1
2
MeasureSpec.getMode(int spec) // 获取模式
MeasureSpec.getSize(int spec) // 获取数值

另外,模式的取值为:

1
2
3
MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST

实际运用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
// 计算过程
...
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
}

12.2.3 onLayout() 函数

onLayout() 是实现所有子控件布局的函数。那关于它自己的布局怎么办呢?是在父控件中由它的父控件完成的。就这样一层一层地向上由各自的父控件完成对自己的布局,直到所有控件的顶层节点。在所有的控件的顶部有一个 ViewRoot,它才是所有控件的祖先节点。

ViewRoot 使用 setFrame(l, t, r, b) 函数中设置自己的位置,设置结束以后才会调用 onLayout(changed, l, t, r, b) 函数来设置内部所有子控件的位置。

示例:

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
public class MyLinLayout extends ViewGroup {
public MyLinLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
// 测量子控件
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获得子控件的宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 得到最大宽度,并且累加高度
height += childHeight;
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
}

res/layout/act_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<com.xxt.xtest.MyLinLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF00FF">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="first view"
android:background="#FF0000"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="second view"
android:background="#00FF00"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="third view"
android:background="#0000FF"/>
</com.xxt.xtest.MyLinLayout>

注意:getMeasuredWidth() 与 getWidth() 获得的值大部分时候是相同的,但含义却是根本不一样的。前者是在 measure() 过程结束后就可以获取到宽度值,而后者是要在 layout() 过程结束后才能获取到宽度值;前者的值是通过 setMeasuredDimension() 函数来进行设置的,而后者的值是通过 layout(left, top, right, bottom) 函数来进行设置的。

12.2.4 获取子控件 margin 值的方法

1. 获取方法及示例

在上面 MyLinLayout 例子的基础上,添加 layout_margin 参数。

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
<?xml version="1.0" encoding="utf-8"?>
<com.xxt.xtest.MyLinLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF00FF">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="first view"
android:background="#FF0000"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="second view"
android:background="#00FF00"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="third view"
android:background="#0000FF"/>
</com.xxt.xtest.MyLinLayout>

重写 generateLayoutParams() 和 generateDefaultLayoutParams(),返回对应的 MarginLayoutParams() 函数的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

重写 onMeasure() 和 onLayout() 函数,修正获取子控件的宽高逻辑。

1
2
3
4
5
6
7
8
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 获得子控件的宽高
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
...
}

最终效果如下图所示。

完整代码:

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
public class MyLinLayout extends ViewGroup {
public MyLinLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
// 测量子控件
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获得子控件的宽高
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 得到最大宽度,并且累加高度
height += childHeight;
width = Math.max(childWidth, width);
}
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 获得子控件的宽高
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
}

2. 原理

在 container 中初始化子控件时,会调用 LayoutParams generateLayoutParams(LayoutParams p) 函数来为子控件生成对应的布局属性,但默认只生成 layout_width 和 layout_height 所对应的布局参数,即在正常情况下调用 generateLayoutParams() 函数生成的 LayoutParams 实例是不能获取到 margin 值的。所以,如我我们还需要与 margin 相关的参数,就只能重写 generateLayoutParams() 函数,返回派生自 LayoutParams 的子类 MarginLayoutParams,根据类的多态性,可以直接将其强转成 MarginLayoutParams 实例。为了安全起见,也可以利用 instanceof 来进行判断。