12.3 精通自定义 View 之封装控件——实现 FlowLayout 容器

返回自定义 View 目录

FlowLayout 容器效果图如下所示:

FlowLayout 容器效果图

12.3.1 XML 布局

先定义一个 style 标签,这是为 FlowLayout 中的 TextView 定义的。

1
2
3
4
5
6
7
8
9
10
<resources>
...
<style name="text_flag">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">4dp</item>
<item name="android:background">@drawable/flag</item>
<item name="android:textColor">#FFFFFF</item>
</style>
</resources>

/res/drawable/flag.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorAccent"/>
<padding
android:top="5dp"
android:bottom="5dp"
android:left="10dp"
android:right="10dp"/>
<corners android:radius="30dp"/>
</shape>

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
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.xxt.xtest.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
style="@style/text_flag"
android:text="Welcome"/>
<TextView
style="@style/text_flag"
android:text="IT 工程师"/>
<TextView
style="@style/text_flag"
android:text="我真是可以的"/>
<TextView
style="@style/text_flag"
android:text="你觉得呢"/>
<TextView
style="@style/text_flag"
android:text="不要只知道挣钱"/>
<TextView
style="@style/text_flag"
android:text="努力 ing"/>
<TextView
style="@style/text_flag"
android:text="I thick i can"/>
</com.xxt.xtest.FlowLayout>
</LinearLayout>

12.3.2 提取 margin 值与重写 onMeasure() 函数

1. 提取 margin 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FlowLayout extends ViewGroup {
...
@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. 重写 onMeasure() 函数

要实现 FlowLayout,必然涉及下面几个问题。
1)何时换行
从效果图中可以看到,FlowLayout 的布局是一行行的,如果当期已经放不下一个控件了,就把这个控件移到下一行显示。所以需要一个变量来计算当前行已经占据的宽度,以判断剩下的空间是否还能容得下下一个控件。
2)如何得到 FlowLayout 的宽度
FlowLayout 的宽度是所有行宽度的最大值,所以我们要记录每一行所占据的宽度值,进而找到所有值中的最大值。
3)如何得到 FlowLayout 的高度
FlowLayout 的高度是每一行高度的总和,而每一行的高度则取该行中所有控件高度的最大值。

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
@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 lineWidth = 0; // 记录每一行的宽度
int lineHeight = 0; // 记录每一行的高度
int width = 0; // 记录整个 FlowLayout 的宽度
int height = 0; // 记录整个 FlowLayout 的高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 计算控件的宽高时,要加上上下左右的 margin 值
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + childWidth > measureWidth) {
// 需要换行
width = Math.max(lineWidth, childWidth);
height += lineHeight;
// 当前行放不下当前控件,而将此控件调到下一行
// 所以将此控件的高度和宽度初始化给 lineWidth、lineHeight
lineWidth = childWidth;
lineHeight = childHeight;
} else {
// 否则累加值 lineWidth,lineHeight 并取最大高度
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 因为最后一行是不会超出 width 范围的,所以需要单独处理
if (i == count - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
}

3. 重写 onLayout() 函数——布局所有子控件

在 onLayout() 函数中需要一个个布局子控件。由于控件要后移和换行,所以我们要标记当前控件的 top 坐标和 left 坐标。然后计算每个控件的 top 坐标和 left 坐标,再调用 layout(int left, int top, int right, int bottom)。

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
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int lineWidth = 0; // 累加当前行的行宽
int lineHeight = 0; // 当前行的行高
int top = 0, left = 0; // 当前控件的 top 坐标和 left 坐标
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;
if (childWidth + lineWidth > getMeasuredWidth()) {
// 如果换行,则当前控件将放到下一行,从最左边开始,所以 left 就是 0;
// 而 top 则需要加上上一行的行高,才是这个控件的 top 坐标
top += lineHeight;
left = 0;
lineHeight = childHeight;
lineWidth = childWidth;
} else {
lineHeight = Math.max(lineHeight, childHeight);
lineWidth += childWidth;
}
// 计算 childView 的 left、top、right、bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
// 将 left 置为下一个子控件的起始点
left += childWidth;
}
}

12.3.3 完整代码

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class FlowLayout extends ViewGroup {
public FlowLayout(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 lineWidth = 0; // 记录每一行的宽度
int lineHeight = 0; // 记录每一行的高度
int width = 0; // 记录整个 FlowLayout 的宽度
int height = 0; // 记录整个 FlowLayout 的高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 计算控件的宽高时,要加上上下左右的 margin 值
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + childWidth > measureWidth) {
// 需要换行
width = Math.max(lineWidth, childWidth);
height += lineHeight;
// 当前行放不下当前控件,而将此控件调到下一行
// 所以将此控件的高度和宽度初始化给 lineWidth、lineHeight
lineWidth = childWidth;
lineHeight = childHeight;
} else {
// 否则累加值 lineWidth,lineHeight 并取最大高度
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 因为最后一行是不会超出 width 范围的,所以需要单独处理
if (i == count - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}
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 count = getChildCount();
int lineWidth = 0; // 累加当前行的行宽
int lineHeight = 0; // 当前行的行高
int top = 0, left = 0; // 当前控件的 top 坐标和 left 坐标
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;
if (childWidth + lineWidth > getMeasuredWidth()) {
// 如果换行,则当前控件将放到下一行,从最左边开始,所以 left 就是 0;
// 而 top 则需要加上上一行的行高,才是这个控件的 top 坐标
top += lineHeight;
left = 0;
lineHeight = childHeight;
lineWidth = childWidth;
} else {
lineHeight = Math.max(lineHeight, childHeight);
lineWidth += childWidth;
}
// 计算 childView 的 left、top、right、bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
// 将 left 置为下一个子控件的起始点
left += childWidth;
}
}
@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);
}
}