Android 使用Compose 自定义ViewGroup 实现流式布局

最近在学compose的滑动列表布局,查看api发现没有提供类似瀑布流相关的,于是打算了解下compose的自定义view,毕竟我对自定义view还是蛮感兴趣的【狗头】,先上效果图自定义竖向滑动的瀑布流布局

一、简介

借助网图介绍下什么是瀑布流式布局:大家有没有发觉这些照片分成左右、最多的还有分成三列的,但是每一行的照片、视频、内容的高度是"错开"的

在Android中我们的recycleviewStaggeredGridLayoutManager可以轻松帮助我们实现该效果,但是在Compose却无法轻易实现因为它只提供了类似,之后官方才推出瀑布流,所以这里学习一种自定义的思想

  • 竖向滑动 LinearLayoutManagerLazyColumn
  • 水平滑动 LinearLayoutManagerLazyRow
  • 网格布局竖向滑动 GridLayoutManagerLazyVerticalGrid
  • 网格布局水平滑动 GridLayoutManagerLazyHorizontalGrid

所以要实现 StaggeredGridLayoutManager需要自定义ViewGroup

二、思路

根据观察可知瀑布流,其实就是将布局填充到内容较少的一列去,根据这个特性就可以去定位排列布局了。 思路,每次定位childview的时候比较所有列的高度,找出最低的列将当前的child定位在下面,这样的话我们就确定的view的高度位置,宽度的话我们根据ViewGroup的宽度以及需要的列数量进行平均分布

三、学习compose的自定义viewgroup的相关知识点

在 Jetpack Compose 中使用Layout可组合,可以使用可组合实现布局视图Layout,其定义为:

less 复制代码
/**
 * [Layout] is the main core component for layout. It can be used to measure and position
 * zero or more layout children.
 *
 * The measurement, layout and intrinsic measurement behaviours of this layout will be defined
 * by the [measurePolicy] instance. See [MeasurePolicy] for more details.
 *
 * For a composable able to define its content according to the incoming constraints,
 * see [androidx.compose.foundation.layout.BoxWithConstraints].
 *
 * Example usage:
 * @sample androidx.compose.ui.samples.LayoutUsage
 *
 * Example usage with custom intrinsic measurements:
 * @sample androidx.compose.ui.samples.LayoutWithProvidedIntrinsicsUsage
 *
 * @param content The children composable to be laid out.
 * @param modifier Modifiers to be applied to the layout.
 * @param measurePolicy The policy defining the measurement and positioning of the layout.
 *
 * @see Layout
 * @see MeasurePolicy
 * @see androidx.compose.foundation.layout.BoxWithConstraints
 */
@Suppress("ComposableLambdaParameterPosition")
@UiComposable
@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
        },
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

翻译可得:

  • content -要布局的子组件。
  • modifier -应用于布局的修饰符。
  • measurePolicy------定义布局的度量和定位的策略。(其实就是对childview进行测量和摆放操作的,是一个lambda表达式,调用每个子组件的 measure(),传递约束条件,得到子组件的大小和位置信息。 - 根据自定义逻辑决定 Layout 的大小(调用 layout() 设置)。 - 为每个子组件调用 place(),放置它们的位置。

常规操作如下:

less 复制代码
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // 对 children 进行测量和放置
    val placeables = measurables.mapIndexed { index, measurable ->
       
        //标准流程:测量每个 child 尺寸,也可以如下重新约束child 的宽高
        measurable.measure(constraints)

        // 测量每个 child 尺寸,重新约束宽度,
        val placeable = measurable.measure(constraints.copy(maxWidth = 100))
   
         // 这句别忘了,返回每个 child 的placeable
        placeable   
    }
    layout(constraints.maxWidth, constraints.maxHeight) {
            // 摆放 children
           placeable.placeRelative( x,y )
      }
    }
}

常规操作:通过遍历measurables,使用measurable.measure(constraints) 得到placeable,获取每个控件的宽高,然后使用 在layout使用placeable.placeRelative(使用该方法每个组件都是相对于viewgroup的边界)进行定位,基于这些知识我们可以愉快的开发了

四、使用Layout自定义瀑布流布局

根据上面的思路写出代码如下,详细流程看代码注释 下面只是让其摆放好位置了,要让其可以滑动该咋办,这个好说为CustomVerticalStaggeredGrid的父布局Column项添加Modifier滑动能力verticalScroll(rememberScrollState()即可

kotlin 复制代码
class ColumnLayoutBean{
    var columnArray:ArrayList<ItemHeight> = ArrayList<ItemHeight>()
    var allHeight:Int = 0 //每一列的总高度
}
class ItemHeight{
    //原始下标
    var originIndex:Int = 0
    //child的高度
    var height:Int = 0
    //当前控件需要放置的Y轴位置
    var lastAllHeight:Int = 0
    //位于哪一列
    var column:Int = 0
}

@Composable
fun CustomVerticalStaggeredGrid(
    modifier: Modifier = Modifier,
    column: Int = 3,  // 自定义的参数,控制展示的行数,默认为 2列
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        Log.d("tgw", "CustomVerticalStaggeredGridmaxWidth:${constraints.maxWidth} ")
        Log.d("tgw", "CustomVerticalStaggeredGridminWidth:${constraints.minWidth} ")
        Log.d("tgw", "CustomVerticalStaggeredGrid maxHeight:${constraints.maxHeight} ")
        Log.d("tgw", "CustomVerticalStaggeredGrid minHeight:${constraints.minHeight} ")
        val columnHeights = Array<ColumnLayoutBean>(column) { ColumnLayoutBean() }
        //存储所有的item项
        var columnArray: ArrayList<ItemHeight> = ArrayList<ItemHeight>()
        //存储每一列的高度总和
        val columnHeightArray = IntArray(column) { 0 }


        // 测量每个 child 尺寸,获得每个 child 的 Placeable 对象
        val placeables = measurables.mapIndexed { index, measurable ->

            // 标准流程:测量每个 child 尺寸,获得 placeable,约束宽度,最大为控件的宽度除以列
            val placeable = measurable.measure(constraints.copy(maxWidth = constraints.maxWidth / column))

            if (index < column) {//初始化列,先占满坑位
                columnHeights[index % column].allHeight = placeable.height

                val item = ItemHeight().apply {
                    this.originIndex = index
                    this.height = placeable.height
                    this.column = index % column
                    this.lastAllHeight = 0
                }
                columnHeights[index % column].columnArray.add(item)
                columnArray.add(item)
                columnHeightArray[index % column] = placeable.height

            }else {
                // 取出高度最小的列的下标,添加
                val minIndex = columnHeightArray.indices.minBy { columnHeightArray[it] }
                columnHeights[minIndex].allHeight += placeable.height
                columnHeightArray[minIndex] = columnHeights[minIndex].allHeight
                //获取当前item的开始时Y轴的放置位置
                var lastHeight = 0
                columnArray.filter { it.column == minIndex }.forEach {
                    lastHeight += it.height
                }
                val item = ItemHeight().apply {
                    this.originIndex = index
                    this.height = placeable.height
                    this.column = minIndex
                    this.lastAllHeight =  lastHeight
                }
                columnHeights[minIndex].columnArray.add(item)
                columnArray.add(item)
            }
            placeable    // 这句别忘了,返回每个 child 的placeable
        }

        // 取出最大列的高度
        val height = columnHeightArray.max()

        // 设置 自定义 Layout 的宽高
        layout(constraints.maxWidth, height) {
            // 摆放每个 child
            placeables.forEachIndexed { index, placeable ->
                val currentColumn = columnArray[index].column
                val x = (constraints.maxWidth / column * currentColumn).toInt()

                placeable.placeRelative(
                    x,
                    columnArray[index].lastAllHeight
                )
            }
        }
    }
}

使用流程如下:

less 复制代码
@Composable
    fun Greeting() {
        Column(
            modifier = Modifier
                .fillMaxSize()
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
  

            // 自定义viewgroup,数据
            val topics = listOf(
                "position1 谁发发发双法防谁发发发双法防谁发发发双法防",
                "position2 ",
                "position3 通天塔",
                "position4 哈哈哈哈哈哈",
                "position5 火花",
                "position6 ",
                "position7 灌灌灌灌",
                "position8 000",
                "position9 天啊啊啊",
                "position10 来来来",
                "position11 哦哦哦哦哦哦哦哦",
                "position12 前期去去去去去去去去去",
                "position13 与i一有意义有意义",
                "position14 iiiiiiiii",
                "position15 谁发发发双法防谁发发发双法防谁发发发双法防",
                "position16 呜呜呜呜呜呜",
                "position17 来了来了",
                "position18 啊啊啊",
                "position19 ",
            )
             
            Text(text = "自定义的瀑布流布局", modifier = Modifier.background(Color(0xFFC56565)))
 Column(modifier = Modifier
            .verticalScroll(rememberScrollState())
            .weight(1f)
            .background(Color(0xFF00FF00))) {
            CustomVerticalStaggeredGrid() {
                for (topic in topics) {
                    Column {
                        Text(text = topic,modifier = Modifier
                            .clickable {
                                Toast
                                    .makeText(
                                        this@CustomMainActivity,
                                        "显示:$topic",
                                        Toast.LENGTH_SHORT
                                    )
                                    .show()
                            }
                            .background(Color(0xFFB88076))
                            .padding(8.dp))
                        Spacer(modifier = Modifier
                            .fillMaxWidth()
                            .height(1.dp)
                            .background(Color(0xFFFFFEFE)))
                    }

                }
            }
        }

    }
}

综上自定义了一个简单的瀑布流的viewgroup布局,借这个例子主要是学习一下自定义viewgroup的流程

参考:

相关推荐
夏非夏2 天前
Kotlin jetpack MVP
android·kotlin
zhangphil2 天前
Kotlin约束泛型参数必须继承自某个父类
kotlin
ch_kexin2 天前
Android kotlin integer-array 存放图片资源ID
android·开发语言·kotlin
IAM四十二3 天前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
jiay23 天前
Kotlin-面向对象之构造函数、实例化和初始化
android·开发语言·kotlin
我怀里的猫3 天前
glide ModelLoader的Key错误使用 可能造成的内存泄漏
android·kotlin·glide
陟彼高冈yu3 天前
第10天:Fragments(碎片)使用-补充材料——‘MainActivity.kt‘解读
android·kotlin·android studio
姑苏风4 天前
《Kotlin实战》-第11章:DSL构建
android·开发语言·kotlin
大耳猫4 天前
Android 解决Java和Kotlin JDK编译版本不一致异常
android·java·kotlin
萌面小侠Plus6 天前
Android笔记(三十五):用责任链模式封装一个App首页Dialog管理工具
android·dialog·笔记·kotlin·责任链模式