一. 前言

继承ViewGroup类可以用来重新定义一种布局,只是这种方式比较复杂,需要去实现ViewGroup的测量和布局过程以及处理子元素的测量和布局。组合View也可以采用这种方式来实现,只是需要处理的细节更复杂而已。

我们这里来实现一个流式布局,什么是流式布局呢?流式布局就是加入此容器的View从左往右依次排列,如果当前行的宽度不够装进下一个View,就会自动将该View放到下一行中去。如下图所示:

1. 需求分析

① 流式布局需要对每个子View进行布局,即从左往右依次摆放,当前行的宽度不够则从下一行开始。

② 流式布局需要测量和计算自身的宽高。

③ 流式布局需要处理marginpadding等细节。

④ 流式布局需要对外提供一些自定义属性,方便用户去使用。比如可以设置行间距和水平间距等等。

2. 实现步骤

① 自定义属性。

② 解析自定义属性以及对外提供一些设置属性的接口等。

③ 重写onMeasure(),实现自身的测量过程。

④ 重写onLayout(),对子View的位置进行布局。

⑤ 使用自定义View

二. 代码实战

1. 自定义属性

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FloatLayout">
<!--水平间距-->
<attr name="horizontalSpace" format="integer"/>
<!--垂直间距-->
<attr name="verticalSpace" format="integer"/>
</declare-styleable>
</resources>

2. 自定义类的构造方法

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
public class FloatLayout extends ViewGroup {

//水平间距
private int horizontalSpace = 10;
//垂直间距
private int verticalSpace = 10;

/**
* 构造方法:Java代码初始化
* @param context
*/
public FloatLayout(Context context) {
super(context);
}

/**
* 构造方法:Xml代码初始化
* @param context
* @param attrs
*/
public FloatLayout(Context context, AttributeSet attrs) {
super(context, attrs);

//解析属性
if (attrs != null){
//获取所有属性值的集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FloatLayout);

//解析每一个属性
horizontalSpace = typedArray.getInteger(R.styleable.FloatLayout_horizontalSpace,horizontalSpace);
verticalSpace = typedArray.getInteger(R.styleable.FloatLayout_verticalSpace,verticalSpace);

//释放资源
typedArray.recycle();
}
}

//设置水平间距
public void setHorizontalSpace(int horizontalSpace) {
this.horizontalSpace = horizontalSpace;
}

//设置垂直间距
public void setVerticalSpace(int verticalSpace) {
this.verticalSpace = verticalSpace;
}
}

3. 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
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
//因为我们需要支持margin,所以需要重写generateLayoutParams方法并创建MarginLayoutParams对象
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
/*@Override
protected ViewGroup.LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}*/
/*@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
}*/

//存储每一行的最大高度
private List<Integer> heights = new ArrayList<>();

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

//获得测量模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

//wrap_content模式下,存储宽和高
int wrapWidth = 0;
int wrapHeight = 0;

//padding的宽度
int widthUsed = getPaddingLeft() + getPaddingRight();

//存储当前行的宽度
int lineWidth = widthUsed;
//存储一行的最大高度
int lineHeight = 0;

//遍历子View进行测量
for (int i = 0; i < getChildCount(); i++) {
//获取子View
View child = getChildAt(i);

//子View为GONE则跳过
if (child.getVisibility() == View.GONE){
continue;
}

//获得一个支持margin的布局参数
MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();

//测量每个child的宽高,每个child可用的最大宽高为 widthSize-padding-margin
measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);

//child实际占据的宽高
int childWidth = child.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
int childHeight = child.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;

//判断这一行还能否装得下这个child
if (lineWidth + childWidth <= widthSize) {
//装得下,累加宽度,并记录最大高度
lineWidth += childWidth + horizontalSpace;

//最大高度
lineHeight = Math.max(lineHeight,childHeight);
}else {
//装不下,需要换行,记录这一行的宽度,高度,下一行的初始宽度,高度

//比较当前行宽度和下一行宽度,取最大值
wrapWidth = Math.max(lineWidth - horizontalSpace, widthUsed + childWidth);

//换行,记录新行的初始宽度
lineWidth = widthUsed + childWidth + horizontalSpace;

//累计当前高度
wrapHeight += lineHeight + verticalSpace;
//保存每一行的最大高度
heights.add(lineHeight);

//记录下一行的初始高度
lineHeight = childHeight;
}

//如果是最后一个child,则将当前记录的最大宽度和当前lineWidth做比较
if (i == getChildCount() - 1){
wrapWidth = Math.max(wrapWidth,lineWidth - horizontalSpace);

//累加高度
wrapHeight += lineHeight;
}
}

//根据测量模式去保存相应的测量宽度
//即如果是MeasureSpec.EXACTLY直接使用父ViewGroup传入的宽和高
//否则设置为自己计算的宽和高,即为warp_content时
setMeasuredDimension(
(widthMode == MeasureSpec.EXACTLY) ? widthSize : wrapWidth,
(heightMode == MeasureSpec.EXACTLY) ? heightSize : wrapHeight
);
}

3. onLayout过程

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
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//获取视图宽度
int width = getWidth();
//记录当前行号
int line = 0;
//存储padding的宽度
int widthUsed = getPaddingLeft() + getPaddingRight();
//记录当前行的宽度
int lineWidth = widthUsed;
//开始的横坐标
int left = getPaddingLeft();
//开始的纵坐标
int top = getPaddingTop();

//遍历所有的子View
for (int i = 0; i < getChildCount(); i++) {
//获取子View
View child = getChildAt(i);

//子View为GONE则跳过
if (child.getVisibility() == View.GONE){
continue;
}

//获得一个支持margin的布局参数
MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();

//child测量的宽
int childWidth = child.getMeasuredWidth();

//判断这一行是否装得下这个child,需要把margin加上
if (lineWidth + childWidth + layoutParams.leftMargin + layoutParams.rightMargin <= width) {
//装得下,累加这一行的宽度
lineWidth += childWidth + horizontalSpace;
}else {
//装不下,需要换行,记录新行的宽度,并设置新的left,right的位置

//重置left
left = getPaddingLeft();

//top累加当前行的最大高度和行间距
top += heights.get(line++) + verticalSpace;

//开始新行,记录宽度
lineWidth = widthUsed + childWidth + horizontalSpace;
}

//计算child的left,top,right,bottom
int lc = left + layoutParams.leftMargin;
int tc = top + layoutParams.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();

//计算child的位置
child.layout(lc,tc,rc,bc);

//left向右移动一个间距
left = layoutParams.rightMargin + rc + horizontalSpace;
}
}

4. 使用

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<swu.xl.floatlayout.FloatLayout
android:layout_width="320dp"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UI界面"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="四大组件,七大布局,三大存储"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="数据库"
android:layout_marginRight="20dp"
android:layout_marginBottom="20dp"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自定义View"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="外部存储"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="性能优化"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UI界面"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="四大组件,七大布局,三大存储"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="数据库"
android:layout_marginRight="20dp"
android:layout_marginBottom="20dp"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自定义View"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="外部存储"
/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="性能优化"
/>

</swu.xl.floatlayout.FloatLayout>

</LinearLayout>

5. 运行结果

6. 源码

FloatLayout

7. 存在的问题

FloatLayout的宽度设置为wrap_content会存在问题,因为没有衡量的标准,会导致所有的子View全部排成一行的。

参考文章

自定义View实践篇(2)- 自定义ViewGroup

自定义ViewGroup支持margin,gravity以及水平,垂直排列