实现带有分隔线的流布局

本文最后更新于:1 年前

实现效果

1、左对齐式流布局

2、左右对齐式流布局


Google 的 FlexboxLayout 不能解决的问题

左对齐时分隔线不能调整高度

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/flexbox_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingEnd="30dp"
android:paddingStart="30dp"
android:paddingTop="10dp"
app:alignItems="center"
app:dividerDrawableVertical="@drawable/ic_divider_horizontal"
app:flexDirection="row"
app:flexWrap="wrap"
app:justifyContent="flex_start"
app:showDividerVertical="beginning|middle|end" />

效果

左右对齐时分隔线不能调整高度且显示效果不理想

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/flexbox_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingEnd="30dp"
android:paddingStart="30dp"
android:paddingTop="10dp"
app:alignItems="center"
app:dividerDrawableVertical="@drawable/ic_divider_horizontal"
app:flexDirection="row"
app:flexWrap="wrap"
app:justifyContent="space_between"
app:showDividerVertical="beginning|middle|end" />

效果

实现过程

重写 onMeasure 方法

1、判断是否存在 adapter,如果不存在,则不需要执行后续计算

1
2
3
if (mFlowLayoutAdapter == null) {
return;
}

2、清除数据,否则会导致每执行一次绘制,则添加一遍数据

1
2
3
4
removeAllViews();
lineList.clear();
Line line = null;
int usedWidth = 0;

3、获取数据并添加到 View

1
2
3
4
List<View> children = mFlowLayoutAdapter.getChildren();
for (View child : children) {
addView(child);
}

此时是不存在分隔线的

4、获取测量的参数

1
2
3
4
5
6
int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();// 去掉 padding,实际可用的宽度
int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();// 去掉 padding,实际可用的高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);// 获取父容器为 child 设置的宽的测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);// 获取父容器为 child 设置的高的测量模式
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);

5、根据宽度,计算 child 的位置,并添加分隔线

(1)遍历 children,如果该 child 不可见,则放弃对其处理,继续处理下一个

1
2
3
4
5
View child = getChildAt(index);
if (child.getVisibility() == View.GONE) {
index++;
continue;
}

(2)如果 child 为一行中的第一个元素,则在 child 的前面添加一个分隔线,并将该分隔线添加到行中,并将行宽度加上该分隔线的宽度

1
2
3
4
5
6
7
8
9
10
int j = 0;
if (line == null) {
line = new Line();
usedWidth = 0;
View dividerView = mFlowLayoutAdapter.getDividerView();
addView(dividerView, index);
line.addChild(dividerView, dividerViewWidth);
usedWidth += dividerViewWidth;
j++;
}

(3)此时,child 必然已不是行的第一个元素,其前面必然有一个分隔线,此时,还需要加上分隔线到 child 的间距

1
usedWidth += dividerViewMinMargin;

(4)然后,才是 child 的宽度,该宽度需要经过计算后再添加

1
2
3
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
int childWidth = child.getMeasuredWidth();
usedWidth += childWidth;

(5)最后,在 child 后面还有一个分隔线和它们的间距需要加上

1
2
usedWidth += dividerViewMinMargin;
usedWidth += dividerViewWidth;

(6)注意,(3)~(5)只是计算添加一个 child 及其分隔线所需的宽度,并未实际将该 child 添加到行中,因为原有行宽度加上这部分宽度有可能导致宽度超过屏幕所能显示的极限,所以添加一个判断,当宽度之和小于宽度极限值时才将该 child 和分隔线加入行

1
2
3
4
5
6
7
if (usedWidth <= widthSize) {
line.addChild(child, childWidth);
View dividerView = mFlowLayoutAdapter.getDividerView();
addView(dividerView, index + j + 1);
line.addChild(dividerView, dividerViewWidth);
j++;
}

(7)当(6)判断出宽度之和已超出宽度极限值时,则将已有行封闭,并将(3)~(5)计算的 child 作为新行的首个元素,在 child 前和后各添加一个分隔线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
lineList.add(line);

line = new Line();
usedWidth = 0;

View dividerView = mFlowLayoutAdapter.getDividerView();
addView(dividerView, index);
line.addChild(dividerView, dividerViewWidth);
usedWidth += dividerViewWidth;
usedWidth += dividerViewMinMargin;
j++;

line.addChild(child, childWidth);
usedWidth += childWidth;
usedWidth += dividerViewMinMargin;
View dividerView1 = mFlowLayoutAdapter.getDividerView();
addView(dividerView1, index + j + 1);

line.addChild(dividerView1, dividerViewWidth);
usedWidth += dividerViewWidth;
j++;

(8)因为按照这种添加方法,当遍历到最后一个 child 时,也会在其后添加一个分隔线,如果循环条件为元素的数量时,会陷入死循环,所以可以判断到最后一个 child 时,将循环标记置为 false,取消后续的循环

1
2
3
4
5
6
while (index < childCount && !isLast) {
if (index == childCount - 1) {
isLast = true;
}
...
}

(9)最后一行可能未满一行,也将其封闭

1
2
3
if (!lineList.contains(line)) {
lineList.add(line);// 把最后一行添加到集合中
}

6、遍历行的集合,累加其高度,并将宽、高设置为测量到的宽高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int totalWidth = widthSize + getPaddingLeft() + getPaddingRight();
this.totalWidth = totalWidth;

int totalHeight = 0;
int lineListSize = lineList.size();
for (int i = 0; i < lineListSize; i++) {
totalHeight += lineList.get(i).getHeight();
}
totalHeight += (lineListSize - 1) * rowMargin;
totalHeight += getPaddingTop();
totalHeight += getPaddingBottom();
this.totalHeight = totalHeight;

setMeasuredDimension(totalWidth, totalHeight);

重写 onLayout 方法

将 paddingLeft 和 paddingTop 作为绘制的起始点,遍历行的集合,让每一行调用其自身方法再次计算其布局位置,相邻两行添加行高

1
2
3
4
5
6
7
8
9
left = getPaddingLeft();
top = getPaddingTop();// 注意:不是 top += getPaddingTop();
int rowMargin = mFlowLayoutAdapter.getRowMargin();
for (int i = 0, size = lineList.size(); i < size; i++) {
Line line = lineList.get(i);
line.layout(left, top); // 分配行的位置,然后再交给每个行去分配 child 的位置
top += line.getHeight();
top += rowMargin;
}

行的布局

1、当为左右对齐式流布局时,需要计算 child 到相邻分隔线的距离,即用总的可用距离除以 child 加上分隔线的数量之和

1
2
3
int childListSize = childList.size();
int marginsWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - width;
int marginWidth = marginsWidth / (childListSize - 1);

此时,可能出现不能整除的情况,我这里做的处理是将余数部分平分,添加到两端的分隔线到最近的 child 之间的距离

1
2
3
int offset = marginsWidth % (childListSize - 1);
int leftOffset = offset >> 1;
int rightOffset = offset - leftOffset;

2、遍历所有元素。当为分隔线时,计算绘制的位置,如果对齐方式为左右对齐,且是最后一个分隔线,left 有可能略向右偏移,top 需要重新计算以便实现居中,right 为 left 加上分隔线的宽度,bottom 为 计算后的 top 加上分隔线的高度

1
2
3
4
5
6
7
8
9
10
if (i % 2 == 0) {
if (i == childListSize - 1) {
if (gravityMode == FlowLayoutAdapter.GRAVITY_MODE_LEFT_AND_RIGHT) {
left += leftOffset;
}
}
realTop = top + ((height - dividerViewHeight) >> 1);
right = left + dividerViewWidth;
bottom = realTop + dividerViewHeight;
}

当为 child 时,计算绘制的位置,如果对齐方式为左右对齐,从第一个 child 起,所有 child 都可能略向左偏移,top 就是传递进来的 top,right 为 left 加上 child 测量出的宽度,bottom 为 top 加上 child 的高度

1
2
3
4
5
6
7
8
9
10
11
else {
realTop = top;
int measuredWidth = child.getMeasuredWidth();
if (i == 1) {
if (gravityMode == FlowLayoutAdapter.GRAVITY_MODE_LEFT_AND_RIGHT) {
left += rightOffset;
}
}
right = left + measuredWidth;
bottom = top + height;
}

3、当一个分隔线或 child 布局计算结束后,左对齐的流布局需要加上最小间距,左右对齐的流布局需要加上平均间距

1
2
3
4
5
6
7
8
switch (gravityMode) {
case FlowLayoutAdapter.GRAVITY_MODE_LEFT:
left += minMargin;
break;
case FlowLayoutAdapter.GRAVITY_MODE_LEFT_AND_RIGHT:
left += marginWidth;
break;
}

源码地址



实现带有分隔线的流布局
https://weichao.io/df678192632e/
作者
魏超
发布于
2018年2月4日
更新于
2022年12月4日
许可协议