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的流程

参考:

相关推荐
alexhilton6 小时前
在Android应用中实战Repository模式
android·kotlin·android jetpack
雨白13 小时前
高阶函数与内联优化
kotlin
&岁月不待人&16 小时前
实现弹窗随键盘上移居中
java·kotlin
移动开发者1号21 小时前
Android Activity状态保存方法
android·kotlin
移动开发者1号21 小时前
Volley源码深度分析与设计亮点
android·kotlin
移动开发者1号2 天前
App主界面点击与跳转启动方式区别
android·kotlin
移动开发者1号2 天前
我用Intent传大图片时竟然崩了,怎么回事啊
android·kotlin
androidwork2 天前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·java·kotlin·androidx
androidwork2 天前
OkHttp 3.0源码解析:从设计理念到核心实现
android·java·okhttp·kotlin
莉樱Yurin2 天前
Kotlin/CLR 让Kotlin走进.NET世界
kotlin