Compose版FlowLayout了解一下~
前言
FlowLayout
是开发中常用的一种布局,并且在面试时,如何自定义FlowLayout
也是一个高频问题
最近Compose
发布正式版了,本文主要是以FlowLayout
为例,熟悉Compose
自定义Layout
的主要流程
本文主要要实现以下效果:
- 自定义
Layout
,从左向右排列,超出一行则换行显示 - 支持设置子
View
间距及行间距 - 当子
View
高度不一致时,支持一行内居上,居中,居下对齐
效果
首先来看下最终的效果
Compose
自定义Layout
流程
在Android View
体系中,自定义Layout
一般有以下几步:
- 测量子
View
宽高 - 根据测量结果确定父
View
宽高 - 根据需要确定子
View
放置位置
在Compose
中其实也是大同小异的
我们一般使用Layout
来测量和布置子项,以实现自定义Layout
,我们首先来实现一个自定义的Column
,如下所示:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
children = content
) { measurables, constraints ->
// 测量布置子项
}
}
Layout
中有两个参数,measurables
是需要测量的子项的列表,而constraints
是来自父项的约束条件
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
//需要测量的子项
val placeables = measurables.map { measurable ->
// 1.测量子项
measurable.measure(constraints)
}
// 2.设置Layout宽高
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
// 在父Layout中定位子项
placeables.forEach { placeable ->
// 3.在屏幕上定位子项
placeable.placeRelative(x = 0, y = yPosition)
// 记录子项的y轴位置
yPosition += placeable.height
}
}
}
}
以上主要就是做了三件事:
- 测量子项
- 测量子项后,根据结果设置父
Layout
宽高 - 在屏幕上定位子项,设置子项位置
然后一个简单的自定义Layout
也就完成了,可以看到,这跟在View
体系中也没有什么区别
下面我们来看下怎么实现一个FlowLayout
吧
自定义FlowLayout
我们首先来分析下,实现一个FlowLayou
需要做些什么?
- 首先我们应该确定父
Layout
的宽度 - 遍历测量子项,如果宽度和超过父
Layout
则换行 - 遍历时同时记录每行的最大高度,最后高度即为每行最大高度的和
- 经过以上步骤,宽高都确定了,就可以设置父
Layout
的宽高了,测量步骤完成 - 接下来就是定位,遍历测量后的子项,根据之前测量的结果确定其位置
流程大概就是上面这些了,我们一起来看看实现
遍历测量,确定宽高
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val parentWidthSize = constraints.maxWidth
var lineWidth = 0
var totalHeight = 0
var lineHeight = 0
// 测量子View,获取FlowLayout的宽高
measurables.mapIndexed { i, measurable ->
// 测量子view
val placeable = measurable.measure(constraints)
val childWidth = placeable.width
val childHeight = placeable.height
//如果当前行宽度超出父Layout则换行
if (lineWidth + childWidth > parentWidthSize) {
//记录总高度
totalHeight += lineHeight
//重置行高与行宽
lineWidth = childWidth
lineHeight = childHeight
totalHeight += lineSpacing.toPx().toInt()
} else {
//记录每行宽度
lineWidth += childWidth + if (i == 0) 0 else itemSpacing.toPx().toInt()
//记录每行最大高度
lineHeight = maxOf(lineHeight, childHeight)
}
//最后一行特殊处理
if (i == measurables.size - 1) {
totalHeight += lineHeight
}
}
//...设置宽高
layout(parentWidthSize, totalHeight) {
}
}
以上就是确定宽高的代码,主要做了以下几件事
- 循环测量子项
- 如果当前行宽度超出父
Layout
则换行 - 每次换行都记录每行最大高度
- 根据测量结果,最后确定父
Layout
的宽高
记录每行的子项与每行最大高度
上面我们已经测量完成了,明确了父Layout
的宽高
不过为了实现当子项高度不一致时居中对齐的效果,我们还需要将每行的子项与每行的最大高度记录下来
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val mAllPlaceables = mutableListOf<MutableList<Placeable>>() // 所有子项
val mLineHeight = mutableListOf<Int>() //每行的最高高度
var lineViews = mutableListOf<Placeable>() //每行放置的内容
// 测量子View,获取FlowLayout的宽高
measurables.mapIndexed { i, measurable ->
// 测量子view
val placeable = measurable.measure(constraints)
val childWidth = placeable.width
val childHeight = placeable.height
//如果行宽超出Layout宽度则换行
if (lineWidth + childWidth > parentWidthSize) {
//每行最大高度添加到列表中
mLineHeight.add(lineHeight)
//二级列表,存放所有子项
mAllPlaceables.add(lineViews)
//重置每行子项列表
lineViews = mutableListOf()
lineViews.add(placeable)
} else {
//每行高度最大值
lineHeight = maxOf(lineHeight, childHeight)
//每行的子项添加到列表中
lineViews.add(placeable)
}
//最后一行特殊处理
if (i == measurables.size - 1) {
mLineHeight.add(lineHeight)
mAllPlaceables.add(lineViews)
}
}
}
上面主要做了三件事
- 每行的最大高度添加到列表中
- 每行的子项添加到列表中
- 将
lineViews
列表添加到mAllPlaceables
中,存放所有子项
定位子项
上面我们已经完成了测量,并且获得了所有子项的列表,现在可以遍历定位了
@Composable
fun ComposeFlowLayout(
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
lineSpacing: Dp = 0.dp,
gravity: Int = Gravity.TOP,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
layout(parentWidthSize, totalHeight) {
var topOffset = 0
var leftOffset = 0
//循环定位
for (i in mAllPlaceables.indices) {
lineViews = mAllPlaceables[i]
lineHeight = mLineHeight[i]
for (j in lineViews.indices) {
val child = lineViews[j]
val childWidth = child.width
val childHeight = child.height
// 根据Gravity获取子项y坐标
val childTop = getItemTop(gravity, lineHeight, topOffset, childHeight)
child.placeRelative(leftOffset, childTop)
// 更新子项x坐标
leftOffset += childWidth + itemSpacing.toPx().toInt()
}
//重置子项x坐标
leftOffset = 0
//子项y坐标更新
topOffset += lineHeight + lineSpacing.toPx().toInt()
}
}
}
}
private fun getItemTop(gravity: Int, lineHeight: Int, topOffset: Int, childHeight: Int): Int {
return when (gravity) {
Gravity.CENTER -> topOffset + (lineHeight - childHeight) / 2
Gravity.BOTTOM -> topOffset + lineHeight - childHeight
else -> topOffset
}
}
要定位一个子项,其实就是确定它的坐标,以上主要做了以下几件事
- 遍历所有子项
- 根据位置确定子项
X
与Y
坐标 - 根据
Gravity
可使子项居上,居中,居下对齐
综上,一个简单的ComposeFlowLayout
就完成了
总结
本文主要实现了一个支持设置子View
间距及行间距,支持子View
居上,居中,居左对齐的FlowLayout
,了解了Compose
自定义Layout
的基本流程
后续更多Compose
相关知识点,敬请期待~