自定义ViewGroup–实现FlowLayout
基本流程
0.自定义容器类继承ViewGroup
需要实现4个构造函数
public class FlowLayout extends ViewGroup {
// 子View二维列表
List<List<View>> allChild = new ArrayList<>();
// 每行的高度,取每行的最大值
List<Integer> itemHeightList = new ArrayList<>();
// 子view占用父控件的空间,取最大行中的数据
int usedWidth = 0;
int usedHeight = 0;
// 父容器内侧上下左右边距
private int paddingLeft = 0;
private int paddingRight = 0;
private int paddingTop = 0;
private int paddingBottom = 0;
// 水平和垂直间距,通过FlowLayout的自定义属性来设置
private int horizontalDivide = 0;
private int verticalDivide = 0;
// 在java代码中new使用
public FlowLayout(@NonNull Context context)
// 在xml中声明时使用
public FlowLayout(@NonNull Context context, @NonNull AttributeSet attrs)
//
public FlowLayout(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr)
@TargetApi(21)
public FlowLayout(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr, int defStyleRes)
}
1.定义容器组件要配置的属性
在value/attrs.xml文件中添加声明
<resources>
<attr name="divide" format="dimension" />
<declare-styleable name="FlowLayout">
<attr name="horizontalDivide" format="dimension">0dp</attr>
<attr name="verticalDivide" format="dimension">0dp</attr>
</declare-styleable>
</resources>
解析xml配置的属性
private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
if (attrs != null) {
TypedArray typedArray = this.getContext().obtainStyledAttributes
(attrs, R.styleable.FlowLayout, defStyleAttr, defStyleRes);
int len = typedArray.getIndexCount();
for (int i = 0; i < len; i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.FlowLayout_horizontalDivide:
this.horizontalDivide = typedArray
.getDimensionPixelOffset(attr, this.horizontalDivide);
break;
case R.styleable.FlowLayout_verticalDivide:
this.verticalDivide = typedArray
.getDimensionPixelOffset(attr, this.verticalDivide);
break;
default:
break;
}
}
// 回收属性数组
typedArray.recycle();
}
}
2.定义容器内子控件可以配置的属性。
<declare-styleable name="FlowLayout_Layout">
<attr name="is_new_line" format="boolean">false</attr>
</declare-styleable>
3.自定义实现FlowLayoutParams,并解析子控件中的布局相关参数
static class FlowLayoutParams extends MarginLayoutParams {
public boolean isNewLine;
//
public FlowLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray typedArray = c.obtainStyledAttributes
(attrs,R.styleable.FlowLayout_Layout);
int len = typedArray.getIndexCount();
for (int i = 0; i < len; i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.FlowLayout_Layout_is_new_line:
isNewLine = typedArray.getBoolean(attr, false);
break;
default:
break;
}
}
typedArray.recycle();
}
public FlowLayoutParams(int width, int height) {
super(width, height);
}
public FlowLayoutParams(MarginLayoutParams source) {
super(source);
}
public FlowLayoutParams(LayoutParams source) {
super(source);
}
}
4.重写父容器中校验及获取子控件布局实例的方法。
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return p instanceof FlowLayoutParams;
}
@Override
protected FlowLayoutParams generateDefaultLayoutParams() {
return new FlowLayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected FlowLayoutParams generateLayoutParams(LayoutParams p) {
return new FlowLayoutParams(p);
}
@Override
public FlowLayoutParams generateLayoutParams(AttributeSet attrs) {
return new FlowLayoutParams(getContext(), attrs);
}
这里顺便提一下这几个generateLayoutParams是在父容器addView的时候用的,当child自身没有LayoutParams时就创建一个默认值
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
5.重写onMeasure来计算子控件宽高并计算自身宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先清除之前的设置
allChild.clear();
itemHeightList.clear();
usedHeight = 0;
usedWidth = 0;
// 记录父容器宽高
int groupWidth = MeasureSpec.getSize(widthMeasureSpec);
int groupWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int groupHeight = MeasureSpec.getSize(heightMeasureSpec);
int groupHeightMode = MeasureSpec.getMode(heightMeasureSpec);
// 为计算行数设置的临时变量
List<View> tmpLine = new ArrayList<>();
int tmpLineWidth = 0;
int tmpLineHeight = 0;
// 遍历子View,计算每个子view的测量尺寸
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 先判断child的可见性,不可见的不参与计算
if (child.getVisibility() == View.VISIBLE) {
// 先拿到子view的宽高参数,这个在addView的时候会由父容器设置好值。
// 通过子view的onMeasure,就可以得到子view自身计算的宽高了,利用该宽高可以做一次初步布局
ViewGroup.LayoutParams params = child.getLayoutParams();
FlowLayoutParams flowLayoutParams = null;
//获取view的margin设置参数
if (params instanceof FlowLayoutParams) {
flowLayoutParams = (FlowLayoutParams) params;
} else {
//不存在时创建一个新的参数
//基于View本身原有的布局参数对象
flowLayoutParams = new FlowLayoutParams(params);
}
// 根据父容器的限制来计算子View的尺寸限制
// 父size使用前要减去两边的padding
// 1.当父容器为EXACTLY时
// 子size>=0 则 EXACTLY + 子size
// 子match_parent,则EXACTLY+父size
// 子wrap_content,则AT_MOST+父size
// 2.当父容器为AT_MOST时,尽可能大
// 子size>=0 则 EXACTLY + 子size
// 子match_parent,则 AT_MOST+父size
// 子wrap_content,则 AT_MOST+父size
// 3.当父容器为UNSPECIFIED时,没指定大小
// 子size>=0,则 EXACTLY + 子size
// 子match_parent,则UNSPECIFIED+0
// 子warp_content,则UNSPECIFIED+0
int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, flowLayoutParams.width);
int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, flowLayoutParams.height);
// 将父容器为子view定制的限制传递给子view,最后会调用子view自身的onMeasure,
child.measure(childWidthSpec, childHeightSpec);
boolean isNewLine = flowLayoutParams.isNewLine;
if (!isNewLine && child.getMeasuredWidth() < groupWidth - paddingLeft - paddingRight - tmpLineWidth) {
//当前行可以放得下
tmpLine.add(child);
tmpLineWidth = tmpLineWidth + child.getMeasuredWidth() + horizontalDivide;
// 行高要考虑margin,行号按该行最高的item计算
tmpLineHeight = Math.max(tmpLineHeight, child.getMeasuredHeight() + flowLayoutParams.topMargin + flowLayoutParams.bottomMargin);
// 如果时最后一个元素了,更新参数
if (i == childCount - 1) {
allChild.add(tmpLine);
usedWidth = Math.max(usedWidth, tmpLineWidth);
usedHeight = usedHeight + tmpLineHeight;//最后一行不需要纵向间距
itemHeightList.add(tmpLineHeight);
}
} else {
// 当前行放不下,进行换行操作
allChild.add(tmpLine);
usedWidth = Math.max(usedWidth, tmpLineWidth);//更新最大行宽
usedHeight = usedHeight + tmpLineHeight + verticalDivide;//更新使用过的宽度
itemHeightList.add(tmpLineHeight);
// 还原临时变量
tmpLine = new ArrayList<>();
tmpLineHeight = 0;
tmpLineWidth = 0;
}
}
}
int realWidth = 0;
int realHeight = 0;
// 现在我们拿到了子view占用的宽高,可以给父容器设置测量宽高了
if (groupWidthMode == MeasureSpec.EXACTLY) {
realWidth = groupWidth;
} else {// 不论是AT_MOST 还是 UNSPECIFIED 都用子view实际占用的宽度
realWidth = usedWidth + paddingLeft + paddingRight;
}
if (groupHeightMode == MeasureSpec.EXACTLY) {
realHeight = groupHeight;
} else {
realHeight = usedHeight + paddingTop + paddingBottom;
}
setMeasuredDimension(realWidth, realHeight);
}
6.重写onLayout来对子控件进行布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 一行一行摆积木,按相对位置摆放
int lineCount = allChild.size();
int currentLeft = paddingLeft;
int currentTop = paddingTop;
for (int i = 0; i < lineCount; i++) {
List<View> line = allChild.get(i);
for (int j = 0; j < line.size(); j++) {
View view = line.get(j);
ViewGroup.LayoutParams params = view.getLayoutParams();
MarginLayoutParams marginParams = null;
//获取view的margin设置参数
if (params instanceof ViewGroup.MarginLayoutParams) {
marginParams = (ViewGroup.MarginLayoutParams) params;
} else {
//不存在时创建一个新的参数
//基于View本身原有的布局参数对象
marginParams = new ViewGroup.MarginLayoutParams(params);
}
int left = currentLeft + marginParams.leftMargin;
int top = currentTop + marginParams.topMargin;
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
currentLeft = currentLeft + view.getMeasuredWidth() + marginParams.rightMargin;
if (j != line.size() - 1) {
currentLeft = currentLeft + horizontalDivide;
}
}
currentLeft = paddingLeft;
currentTop = currentTop + itemHeightList.get(i) + verticalDivide;
}
}