前言
最近在做的项目决定用流式布局来展示历史记录,刚好自己也想学习自定义 ViewGroup,所以就参考了其他的一些文章,写了一个 FlowLayout(流式布局),效果如下:

代码实现(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
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177/**
* @author Feng Zhaohao
* Created on 2019/11/10
*/
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private List<Rect> mChildrenPositionList = new ArrayList<>(); // 记录各子 View 的位置
private int mMaxLines = Integer.MAX_VALUE; // 最多显示的行数,默认无限制
private int mVisibleItemCount; // 可见的 item 数
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 清除之前的位置
mChildrenPositionList.clear();
// 测量所有子元素(这样 child.getMeasuredXXX 才能获取到值)
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int[] a = helper(widthSize);
int measuredHeight = 0;
// EXACTLY 模式:对应指定大小和 match_parent
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
}
// AT_MOST 模式,对应 wrap_content
else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = a[0];
}
int measuredWidth = 0;
if (widthMode == MeasureSpec.EXACTLY) {
measuredWidth = widthSize;
}
else if (widthMode == MeasureSpec.AT_MOST) {
measuredWidth = a[1];
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
* 在 wrap_content 情况下,得到布局的测量高度和测量宽度
* 返回值是一个有两个元素的数组 a,a[0] 代表测量高度,a[1] 代表测量宽度
*/
private int[] helper(int widthSize) {
boolean isOneRow = true; // 是否是单行
int width = getPaddingLeft(); // 记录当前行已有的宽度
int height = getPaddingTop(); // 记录当前行已有的高度
int maxHeight = 0; // 记录当前行的最大高度
int currLine = 1; // 记录当前行数
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 获取当前子元素的 margin
LayoutParams params = child.getLayoutParams();
MarginLayoutParams mlp;
if (params instanceof MarginLayoutParams) {
mlp = (MarginLayoutParams) params;
} else {
mlp = new MarginLayoutParams(params);
}
// 记录子元素所占宽度和高度
int childWidth = mlp.leftMargin + child.getMeasuredWidth() + mlp.rightMargin;
int childHeight = mlp.topMargin + child.getMeasuredHeight() + mlp.bottomMargin;
maxHeight = Math.max(maxHeight, childHeight);
// 判断是否要换行
if (width + childWidth + getPaddingRight() > widthSize) {
// 加上该行的最大高度
height += maxHeight;
// 重置 width 和 maxHeight
width = getPaddingLeft();
maxHeight = childHeight;
isOneRow = false;
currLine++;
if (currLine > mMaxLines) {
break;
}
}
// 存储该子元素的位置,在 onLayout 时设置
Rect rect = new Rect(width + mlp.leftMargin,
height + mlp.topMargin,
width + childWidth - mlp.rightMargin,
height + childHeight - mlp.bottomMargin);
mChildrenPositionList.add(rect);
// 加上该子元素的宽度
width += childWidth;
}
int[] res = new int[2];
res[0] = height + maxHeight + getPaddingBottom();
res[1] = isOneRow? width + getPaddingRight() : widthSize;
return res;
}
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 布置子 View 的位置
int n = Math.min(getChildCount(), mChildrenPositionList.size());
for (int i = 0; i < n; i++) {
View child = getChildAt(i);
Rect rect = mChildrenPositionList.get(i);
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
mVisibleItemCount = n;
}
/**
* 设置 Adapter
*/
public void setAdapter(Adapter adapter) {
// 移除之前的视图
removeAllViews();
// 添加 item
int n = adapter.getItemCount();
for (int i = 0; i < n; i++) {
ViewHolder holder = adapter.onCreateViewHolder(this);
adapter.onBindViewHolder(holder, i);
View child = holder.itemView;
addView(child);
}
}
/**
* 设置最多显示的行数
*/
public void setMaxLines(int maxLines) {
mMaxLines = maxLines;
}
/**
* 获取显示的 item 数
*/
public int getVisibleItemCount() {
return mVisibleItemCount;
}
public abstract static class Adapter<VH extends ViewHolder> {
public abstract VH onCreateViewHolder(ViewGroup parent);
public abstract void onBindViewHolder(VH holder, int position);
public abstract int getItemCount();
}
public abstract static class ViewHolder {
public final View itemView;
public ViewHolder(View itemView) {
if (itemView == null) {
throw new IllegalArgumentException("itemView may not be null");
}
this.itemView = itemView;
}
}
}
详细说明请看注释,这里就不多说了。下面看下使用方法:
使用方法
使用方法和 RecyclerView 类似,先编写 item 的布局,然后编写 Adapter,最后给 FlowLayout 设置 Adapter 即可显示数据。
1. 编写 item 布局
1 |
|
简单起见,布局只有一个 TextView。
注意:不能在 xml 中设置 margin 属性,需在代码中设置,后面会说明。
2. 编写 Adapter
1 | public class FlowAdapter extends FlowLayout.Adapter<FlowAdapter.FlowViewHolder> { |
如果想要给子 View 设置 margin,需在 onCreateViewHolder 方法创建 View 时设置:1
2
3
4
5// 给 View 设置 margin
ViewGroup.MarginLayoutParams mlp = new ViewGroup.MarginLayoutParams(view.getLayoutParams());
int margin = BaseUtil.dip2px(mContext, 5);
mlp.setMargins(margin, margin, margin, margin);
view.setLayoutParams(mlp);
3. 主活动中使用 FlowLayout
1 | private static final String TAG = "TestActivity"; |