注册

Compose版FlowLayout了解一下~

前言


FlowLayout是开发中常用的一种布局,并且在面试时,如何自定义FlowLayout也是一个高频问题
最近Compose发布正式版了,本文主要是以FlowLayout为例,熟悉Compose自定义Layout的主要流程
本文主要要实现以下效果:



  1. 自定义Layout,从左向右排列,超出一行则换行显示
  2. 支持设置子View间距及行间距
  3. 当子View高度不一致时,支持一行内居上,居中,居下对齐

效果


首先来看下最终的效果


Compose自定义Layout流程


Android View体系中,自定义Layout一般有以下几步:



  1. 测量子View宽高
  2. 根据测量结果确定父View宽高
  3. 根据需要确定子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
}
}
}
}

以上主要就是做了三件事:



  1. 测量子项
  2. 测量子项后,根据结果设置父Layout宽高
  3. 在屏幕上定位子项,设置子项位置

然后一个简单的自定义Layout也就完成了,可以看到,这跟在View体系中也没有什么区别
下面我们来看下怎么实现一个FlowLayout


自定义FlowLayout


我们首先来分析下,实现一个FlowLayou需要做些什么?



  1. 首先我们应该确定父Layout的宽度
  2. 遍历测量子项,如果宽度和超过父Layout则换行
  3. 遍历时同时记录每行的最大高度,最后高度即为每行最大高度的和
  4. 经过以上步骤,宽高都确定了,就可以设置父Layout的宽高了,测量步骤完成
  5. 接下来就是定位,遍历测量后的子项,根据之前测量的结果确定其位置

流程大概就是上面这些了,我们一起来看看实现


遍历测量,确定宽高


    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) {

}
}

以上就是确定宽高的代码,主要做了以下几件事



  1. 循环测量子项
  2. 如果当前行宽度超出父Layout则换行
  3. 每次换行都记录每行最大高度
  4. 根据测量结果,最后确定父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)
}
}
}

上面主要做了三件事



  1. 每行的最大高度添加到列表中
  2. 每行的子项添加到列表中
  3. 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
}
}

要定位一个子项,其实就是确定它的坐标,以上主要做了以下几件事



  1. 遍历所有子项
  2. 根据位置确定子项XY坐标
  3. 根据Gravity可使子项居上,居中,居下对齐

综上,一个简单的ComposeFlowLayout就完成了


总结


本文主要实现了一个支持设置子View间距及行间距,支持子View居上,居中,居左对齐的FlowLayout,了解了Compose自定义Layout的基本流程
后续更多Compose相关知识点,敬请期待~


本文的所有相关代码


Compose版FlowLayout


0 个评论

要回复文章请先登录注册