自定义ViewGroup打造流式布局
流式布局
流式布局是一种特殊的布局模式,生活中有很多地方会使用到流式布局,比如网站的标签云就是使用的流式布局,流式布局的特点是,按方向一个一个的排列元素,当一行无法容纳新元素的时候,会自动换行,将新元素放置到下一行,如下图局势一个典型的流式布局。

自定义ViewGroup打造流式布局
还是老规矩,介绍之前贴出Demo效果图,看看是否为你想要的样子。

- 自定义View的步骤
自定义一个View往往有三个方法需要注意。
onMeasure(); 用于测量View的大小
onLayout(); 用于分配View的位置,定义布局
onDraw(); 绘制View
由于网上关于这三个函数的讲解已经很多了,而且都是大神级别的人物写的,所以我就不献丑了,只贴出一些我觉得好的博客。
- 实战编码
如果你详细阅读了上面3篇博客,那么你一定对View的绘制流程有了基本了解,现在我们开始实际编码。首先是自定义一个类FlowLayout继承自ViewGroup。我先贴出全部源码,然后讲解。
|
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
/**
* 流式布局
*
* @author Jay
*/
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public FlowLayout(Context context) {
super(context, null);
}
/**
* 测量视图
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
// 最终测量的大小
int width = 0;
int height = 0;
// 每一行的宽高
int lineWidth = 0;
// 第N-1行的高度
int tempHeight = 0;
// 获取有多少个子布局
int childCount = getChildCount();
for (int index = 0; index < childCount; index++) {
// 获取子View
View child = getChildAt(index);
// 如果子View不可见
if (child.getVisibility() == View.GONE) {
continue;
}
// 测量子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获取子View的布局参数
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
// 获取子布局的宽高
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
// 如果一行放不下了,那么这个子View应该放到下一行
if (lineWidth + childWidth + getPaddingLeft() + getPaddingRight() > sizeWidth) {
tempHeight = height;// 记录N-1行的高度
width = Math.max(width, lineWidth);
lineWidth = childWidth;
height = height + childHeight;
} else {
lineWidth = lineWidth + childWidth;
height = Math.max(height, tempHeight + childHeight);
}
}
// 加上边距
height = height + getPaddingTop() + getPaddingBottom();
width = width + getPaddingLeft() + getPaddingRight();
Log.v("x", height + "/" + width);
setMeasuredDimension(
//
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width,
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height);
}
/**
* 视图布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取视图的宽度
int width = getWidth();
// 布局坐标,考虑到布局有可能设置了内边距
int left = getPaddingLeft();
int top = getPaddingTop();
// 一行中最高的一个view的高度
int maxHeight = 0;
int viewCount = getChildCount();
for (int index = 0; index < viewCount; index++) {
View child = getChildAt(index);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
// 换行
if (left + childWidth + getPaddingRight() > width) {
left = getPaddingLeft();
top = top + maxHeight;
maxHeight = 0;
} else {
// 得到最高高度
maxHeight = Math.max(childHeight, maxHeight);
}
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;
}
}
/**
* 返回默认的LayoutParams,如果一个视图使用addView(View)添加而没有指定LayoutParams的时候
* 使用MarginLayoutParams当做LayoutParams
* 这个函数返回的类型就是View.getLayoutParams的返回值类型
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
|
首先自定义一个类FlowLayout去继承ViewGroup然后覆写其三个构造函数,还有覆写onMeasure()和onLayout()。下面还覆写了一个generateLayoutParams(),这个函数的作用是给子View也就是FlowLayout里面包含的View去设置默认的LayoutParams,这里使用MarginLayoutParams的原因是我们的子View有可能会设置外边距。
先来看看onMeasure()方法。
总体思路是如果测量模式是MeasureSpec.EXACTLY(match_parent),那么直接使用传递进来的Size,如果为自适应大小,那么我们就需要测量了,测量的思路是,遍历所有的子View,如果当前一行已经使用了的宽度+将放入的View的宽度大于总宽度,说明要换行了,将这个View放入下一行,具体请看代码,这样遍历完所有的View以后,则获取了FlowLayout的宽高,然后根据布局文件中宽高的配置设置大小。
onLayout()方法
onLayout()是用来分配子布局的位置,基本思路和上面onMeasure()的一样,遍历所有子View,然后根据每个子View的位置获取具体坐标然后设置,具体请看代码实现。
注意:计算需要考虑到内外边距,所以计算得细心。当然如果你不想自己实现,也可以直接下载源码中的FlowLayout.java文件,然后在布局文件中使用。
在布局文件中使用FlowLayout。使用方法和普通控件是一样的,只是需要带上包名。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.jay.flowlayout.FlowLayout
android:padding="5dp"
android:id="@+id/flowlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#E5E5F5">
</com.jay.flowlayout.FlowLayout>
</LinearLayout>
|
在Acitivity中进行动态添加控件就不再贴出了,可自行查看源码。

浙公网安备 33010602011771号