实战-自定义ViewGroup 流动布局(FlowLayout)

安卓自定义 ViewGroup 需要注意的地方

至少需要提供 width, 和 height 两个属性

同样地,如果要使用自定义的属性,那么就需要创建自己的名字空间,在 Android Studio 中,第三方的控件都使用如下代码来引入名字空间。

xmlns:custom="http://schemas.android.com/apk/res-auto"

流动布局手写精简版

  • 增加了 ‘center’ 居中等三种排列方式
  • 额外支持 padding 属性
  • layout_newline 属性支持自定义换行(类似’\n’的换行效果)

参考

下一步升级

  • 逆序排列子 view
  • 倒序排列子 view
  • 使每行 view 均分剩余空间
  • 可尝试增加目前流行的 tag 效果

FlowLayout.java

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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package cn.lik.view;

import java.util.ArrayList;
import java.util.List;

import cn.lik.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
* 流式布局
*
* @author l1k
*/
public class FlowLayout extends ViewGroup {
private static final String TAG = FlowLayout.class.getSimpleName();
private static final boolean DEBUG = false;

/**
* 存储所有的View,按行记录
*/
private ArrayList<List<View>> mAllViews = new ArrayList<List<View>>();
/**
* 记录每一行的最大高度
*/
private ArrayList<Integer> mLineHeight = new ArrayList<Integer>();


/**
* The current value of the {@link AlignItems}, the default value is
* {@link AlignItems#CENTER}.
*
* @see AlignItems
*/
private int mAlignItems;

public FlowLayout(Context context) {
this(context, null);
}

public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlexLayout, defStyleAttr, 0);
mAlignItems = a.getInt(R.styleable.FlexLayout_alignItems, AlignItems.CENTER);
a.recycle();
}

@AlignItems
public int getAlignItems() {
return mAlignItems;
}

public void setAlignItems(@AlignItems int alignItems) {
if (mAlignItems != alignItems) {
mAlignItems = alignItems;
requestLayout();
}
}

@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}

@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}

@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

/**
* 负责设置子控件的测量模式和大小 根据所有子控件设置自己的宽和高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获得它的父容器为它设置的测量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

if (DEBUG) {
Log.i(TAG, "onMeasure: sizeWidth = " + sizeWidth + ", sizeHeight = " + sizeHeight);
}

// 如果是warp_content情况下,记录宽和高
int width = 0;
int height = 0;
/**
* 记录每一行的宽度,width不断取最大宽度
*/
int lineWidth = 0;
/**
* 每一行的高度,累加至height
*/
int lineHeight = 0;

int cCount = getChildCount();

final int paddingHorizontal = getPaddingLeft() + getPaddingRight();

// 遍历每个子元素
for (int i = 0; i < cCount; i++) {
final View child = getChildAt(i);
// 测量每一个child的宽和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到child的lp
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

// 当前子空间实际占据的宽度
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
// 当前子空间实际占据的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
/**
* 如果加入当前child,则超出最大宽度,则的到目前最大宽度给width,累加height 然后开启新行
*/
if ((i!=0) && (lp.newline || (lineWidth + childWidth > sizeWidth - paddingHorizontal))) {
width = Math.max(Math.max(lineWidth, childWidth), width);// 取最大
lineWidth = childWidth; // 开启新行,记录宽度
// 叠加当前高度,
height += lineHeight;
// 开启记录下一行的高度
lineHeight = childHeight;// 开启新行, 记录高度
} else { // 否则累加值lineWidth,lineHeight取最大高度
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 如果是最后一个,则将当前记录的最大宽度和当前lineWidth做比较
if (i == cCount - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}

super.setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth
: width + getPaddingLeft() + getPaddingRight(),
(modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
: height + getPaddingTop() + getPaddingBottom());
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mLineHeight.clear();

int width = getWidth();
int lineWidth = 0;
int lineHeight = 0;

int left = getPaddingLeft();
int top = getPaddingTop();
final int paddingHorizontal = left + getPaddingRight();

// 存储每一行所有的childView
List<View> lineViews = new ArrayList<View>();
int cCount = getChildCount();
// 遍历所有的孩子
for (int i = 0; i < cCount; i++) {
final View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
// 当前子空间实际占据的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

// 如果已经需要换行
if ((i!=0) && (lp.newline || (childWidth + lineWidth > width - paddingHorizontal))) {
// 记录这一行所有的View以及最大高度
mLineHeight.add(lineHeight);
// 将当前行的childView保存,然后开启新的ArrayList保存下一行的childView
mAllViews.add(lineViews);

lineWidth = 0;// 重置宽
lineHeight = 0;//重置高
lineViews = new ArrayList<View>();
}
/**
* 如果不需要换行,则累加
*/
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
lineViews.add(child);
}
// 记录最后一行
mLineHeight.add(lineHeight);
mAllViews.add(lineViews);

// 得到总行数
int lineNums = mAllViews.size();
for (int i = 0; i < lineNums; i++) {
// 每一行的所有的views
lineViews = mAllViews.get(i);
// 当前行的最大高度
lineHeight = mLineHeight.get(i);

if (DEBUG) {
Log.e(TAG, "第" + i + "行 :" + lineViews.size() + " , " + lineViews);
Log.e(TAG, "第" + i + "行, :" + lineHeight);
}

// 遍历当前行所有的View
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

// 计算childView的left,top,right,bottom
int lc = 0;
int tc = 0;
int rc = 0;
int bc = 0;

switch (mAlignItems) {
case AlignItems.FLEX_START:
lc = left + lp.leftMargin;
tc = top + lp.topMargin;
rc = lc + child.getMeasuredWidth();
bc = tc + child.getMeasuredHeight();
break;
case AlignItems.FLEX_END:
lc = left + lp.leftMargin;
tc = top + lineHeight - child.getMeasuredHeight() - lp.bottomMargin;
rc = lc + child.getMeasuredWidth();
bc = tc + child.getMeasuredHeight();
break;
case AlignItems.CENTER:
lc = left + lp.leftMargin;
tc = top + lp.topMargin
+ (lineHeight - lp.topMargin - child.getMeasuredHeight() - lp.bottomMargin) / 2;
rc = lc + child.getMeasuredWidth();
bc = tc + child.getMeasuredHeight();
break;
default:
throw new IllegalStateException("Invalid alignItems is set: " + mAlignItems);
}

child.layout(lc, tc, rc, bc);

left += child.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
}
left = getPaddingLeft();
top += lineHeight;
}

}

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* need newline or not
*/
public boolean newline = false;

public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlexLayout_Layout);
newline = a.getBoolean(R.styleable.FlexLayout_Layout_layout_newline, false);
a.recycle();
}

public LayoutParams(ViewGroup.LayoutParams lp) {
super(lp);
}

public LayoutParams(ViewGroup.MarginLayoutParams lp) {
super(lp);
}

/**
* Copy constructor. Clones the width, height, margin values, newline
*
* @param lp The layout params to copy from.
*/
public LayoutParams(LayoutParams lp) {
super(lp);
this.newline = lp.newline;
}

/**
* Copy constructor. Clones the width, height
*
* @param lp The layout params to copy from.
*/
public LayoutParams(int width, int height) {
super(width, height);
}

/**
* Copy constructor. Clones the width, height, newline
*
* @param lp The layout params to copy from.
*/
public LayoutParams(int width, int height, boolean newline) {
super(width, height);
this.newline = newline;
}

}

}

AlignItems.java

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
/*
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cn.lik.view.flexbox;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** This attribute controls the alignment along the cross axis. */
@Retention(RetentionPolicy.SOURCE)
public @interface AlignItems {

/** Flex item's edge is placed on the cross start line. */
int FLEX_START = 0;

/** Flex item's edge is placed on the cross end line. */
int FLEX_END = 1;

/** Flex item's edge is centered along the cross axis. */
int CENTER = 2;
}

res/values/attrs.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlexLayout">
<attr name="alignItems">
<enum name="flex_start" value="0" />
<enum name="flex_end" value="1" />
<enum name="center" value="2" />
</attr>
</declare-styleable>
<declare-styleable name="FlexLayout_Layout">
<attr name="layout_newline" format="boolean" />
</declare-styleable>
</resources>