转载请标明出处:blog.csdn.net/lmj62356579… ,本文出自【张鸿洋的博客】
流式布局是一种在 ViewGroup 中将子 View 从左到右依次排列的方式。如果当前行剩余空间不足,子 View 会自动换行到下一行。这种布局常用于关键字搜索、热门标签等场景。
自定义 ViewGroup 实现流式布局的关键步骤包括:
- 重写 onMeasure 方法:完成子 View 的测量,并根据子 View 的测量结果确定 ViewGroup 自身的测量宽高。
- 重写 onLayout 方法:确定子 View 的位置,并调用 child.layout(left, top, right, bottom) 方法进行布局。
1. 实现 onMeasure 方法
首先,需要实现 ViewGroup 的 onMeasure 方法。主要处理两个问题:将测量过程传递到子 View 和确定 ViewGroup 自己的测量宽高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//ViewGroup可用宽度
int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
//ViewGroup可用高度
int availableHeight = heightSize - getPaddingTop() - getPaddingBottom();
//记录行宽,需要根据子View的宽度计算,但是不能超过availableWidth(ViewGroup可用宽度)
int lineWidth = 0;
//记录行高,需要根据子View的宽度计算,但是不能超过availableHeight(ViewGroup可用高度)
int lineHeight = 0;
//记录ViewGroup宽度,需要根据每行的宽度计算
int width = 0;
//记录ViewGroup高度,需要根据每行的高度计算
int height = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//将测量过程传递到子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
int childWidthWithMargin =
child.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
int childHeightWithMargin =
child.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
//如果子View的宽度+当前行宽度,大于ViewGroup的可用宽度
//那么就表示子View需要换行
if (lineWidth + childWidthWithMargin > availableWidth) {
//换行需要重新初始化行的宽高。
//但是初始化行的宽高之前,需要比较上一行宽度和记录的ViewGroup宽度,两者取最大值,并累加上一行的高度
width = Math.max(width, lineWidth);
height += lineHeight;
//
lineWidth = 0;
lineHeight = 0;
}
//累加行宽度
lineWidth += childWidthWithMargin;
//获取当前行最大高度
lineHeight = Math.max(lineHeight, childHeightWithMargin);
}
//如果是最后一个View,需要比较最后一行的宽度和记录的ViewGroup宽度,两者取最大值,并累加最后一行的高度
width = Math.max(width, lineWidth);
height += lineHeight;
height = Math.min(height, availableHeight);
//根据模式,如果是MeasureSpec.EXACTLY则直接使用父ViewGroup传入的宽和高,否则设置为自己计算的宽和高
int measureWidth = widthMode == MeasureSpec.EXACTLY ?
widthSize : width + getPaddingLeft() + getPaddingRight();
int measureHeight = heightMode == MeasureSpec.EXACTLY ?
heightSize : height + getPaddingTop() + getPaddingBottom();
//设置ViewGroup的测量宽高
setMeasuredDimension(measureWidth, measureHeight);
}
上述代码通过测量子 View 的尺寸,并计算出 ViewGroup 的最终宽高。
2. 实现 onLayout 方法
接下来,实现 onLayout 方法,确定每一个子 View 的位置。
//存储所有的View
protected List<List<View>> mAllViews = new ArrayList<>();
//存储当前行所有的View
protected List<View> mLineViews = new ArrayList<>();
//存储每一行的行宽
protected List<Integer> mLineWidths = new ArrayList<>();
//存储每一行的行高
protected List<Integer> mLineHeights = new ArrayList<>();
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mLineViews.clear();
mLineWidths.clear();
mLineHeights.clear();
//记录当前行宽度
int lineWidth = 0;
//记录当前行高度
int lineHeight = 0;
//ViewGroup的可用宽度
int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
//子View的宽度加左右margin值
int childWidthWithMargin = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
//子View的高度加左右margin值
int childHeightWithMargin = child.getMeasuredHeight() + params.topMargin + params.topMargin;
//换行
if (lineWidth + childWidthWithMargin > availableWidth) {
//记录上一行的宽度、高度以及View的一个集合
mLineHeights.add(lineHeight);
mLineWidths.add(lineWidth);
mAllViews.add(mLineViews);
//换行后,重新初始化记录的行宽高,初始化当前行的View集合
lineWidth = 0;
lineHeight = 0;
mLineViews = new ArrayList<>();
}
//累加子View的宽度
lineWidth += childWidthWithMargin;
//比较高度
lineHeight = Math.max(lineHeight, childHeightWithMargin);
//记录改行的View
mLineViews.add(child);
}
//for循环中没有记录最后一行的宽高,需要单纯记录
mLineHeights.add(lineHeight);
mLineWidths.add(lineWidth);
mAllViews.add(mLineViews);
/**
* 开始摆放View
* */
//第一个View的top位置
int top = getPaddingTop();
//遍历每一行
for (int i = 0; i < mAllViews.size(); i++) {
//获取当前行的View集合
List<View> lineViews = mAllViews.get(i);
lineHeight = mLineHeights.get(i);
lineWidth = mLineWidths.get(i);
//每行第一个View的left位置
int left = getPaddingLeft();
//遍历每一行中的View
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
//确定子View的left、top、right、bottom
int childLeft = left + params.leftMargin;
int childTop = top + params.topMargin;
int childRight = childLeft + childWidth;
int childBottom = childTop + childHeight;
child.layout(childLeft, childTop, childRight, childBottom);
//初始化下一个View的left位置
left = childRight + params.rightMargin;
}
//换行后,top需要累加上一行的高度
top += lineHeight;
}
}
上述代码通过计算每一行子 View 的位置,并在换行时重新计算位置。
3. 使用 FlowLayout
最后,在布局文件中使用自定义的 FlowLayout:
<com.example.viewserise.flow.TagFlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#eeeeee"
android:padding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/bg_test"
android:paddingStart="15dp"
android:paddingTop="8dp"
android:paddingEnd="15dp"
android:paddingBottom="8dp"
android:text="呵呵呵"
android:textColor="#ffffff"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/bg_test"
android:paddingStart="15dp"
android:paddingTop="8dp"
android:paddingEnd="15dp"
android:paddingBottom="8dp"
android:text="旅行箱"
android:textColor="#ffffff"
android:textSize="14sp" />
<!-- 更多的 TextView 元素... -->
</com.example.viewserise.flow.TagFlowLayout>