Compose版FlowLayout了解一下~

·  阅读 1795
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

分类:
Android
标签: