Compose 滑动布局中嵌套的Grid网格布局

在这里插入图片描述
上面是需求图
整个布局是可以垂直方向滑动的,同时内部需要用垂直方向的网格布局来显示图片。并且通过让宽度或间距平分剩余空间,来实现对齐效果。如果丢个FlowRow代替Grid,是不能实现对齐的,基本会头重脚轻

同向可滑动嵌套的隐患

众所周知,同向的可滑动嵌套可能会导致性能问题。比如A布局允许垂直方向滑动,B是A布局中的一个RecyclerView,高度为wrapContent,由于B的父布局A可以垂直滑动,A对B的高度不做限制,B会一口气把所有的元素都加载出来,导致RecyclerView的延迟加载和回收逻辑都失效,如果数据太多的话会导致性能问题。如果是分页列表的话,那么它还会一直处于加载到底部-》触发加载下一页-》加载到底部的循环中,导致从性能问题进化到业务问题。

Compose中为了防止这个情况,对同向的滑动进行了限制,在大多数的同向可滑动布局嵌套时会直接抛出异常(除非child在对应方向上设置了具体的尺寸,比如B在A里面,B设置了Modifier.height(30.dp),这样可以正常运行)。

有一些场景,我们确定在特定的业务场景下,集合元素不会很多,而且UI就要求用高度自适应的网格布局来显示内容。在使用View时我们有很多方法,有直接的网格布局,也可以头硬上RecyclerView(虽然可能会导致问题,但View体系中并没有禁止开发者这么做,后果自负就是了)。
但是在Compose中,由于只提供了LazyXXXGrid,并没有那种不需要滑动,高度自适应,可以直接在滑动布局内部使用的Grid,有的时候会导致困扰——我不想把整个滑动界面改成LazyXXXGrid,我也确定Grid中的元素不会那么多,甚至只有几个,绝对不影响性能。
这个时候我们还只能自己实现一个简单的Grid了,直接上代码。

代码


import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
 * 由于Compose只提供了懒加载的LazyVerticalGrid,又不允许同向的嵌套滑动,会直接抛出异常,所以需要我们自己实现Grid布局(很多时候我们知道嵌套滑动会导致懒加载失效,内容全部加载等问题,但是我们也确定一些使用场景里Grid的内容不会过多,就是想要使用Grid)。
 *
 * 用类似LazyVerticalGrid的布局效果:会在遵守[minItemSize]的前提下尽可能的在每一行放更多的项,并让他们平分剩余空间。
 *
 * 由于是简单实现,所以布局中的每一项会设置为正方形,高度向宽度对齐
 *
 * 比如总宽度为100,minItemSize为30,那么最终会有3列,每列宽度为100/3(这个例子中没有考虑[horizontalSpace])
 * @param minItemSize 子项的最小尺寸。在布局的最大宽度小于这个尺寸时,会让子项尺寸为最大宽度;否则子项的宽度至少为这个尺寸
 * @param horizontalSpace 水平方向的间距,只有一列时间距不会生效
 * @param verticalSpace 垂直方向的间距,只有一行时间距不会生效
 * @param content 直接往里丢内容即可,参考[Row],[Column]等布局的content参数
 */
@Composable
fun EasyGrid(
    minItemSize: Dp,
    modifier: Modifier = Modifier,
    horizontalSpace: Dp = 0.dp,
    verticalSpace: Dp = 0.dp,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables: List<Measurable>, constraints: Constraints ->
        val horizontalSpaceValue = horizontalSpace.toPx()
        val verticalSpaceValue = verticalSpace.toPx()
        val totalWidth = constraints.maxWidth
        val minItemWidth = minItemSize.toPx()
        // 用于记录最终的列数,至少有一列
        var numItems = 1
        // 剩余多少可用宽度
        var remainWidth = totalWidth.toFloat() - minItemWidth
        // 计算可以摆放的项的最大数量,同时考虑间隔
        while (remainWidth >= minItemWidth + horizontalSpaceValue) {
            remainWidth -= (minItemWidth + horizontalSpaceValue)
            numItems++
        }
        // 此时numItems已经是计算完的列数,再来计算平均宽度
        val finalItemWidth = when {
            // 只有一列时,子项宽度就和布局的总宽度一致
            numItems == 1 -> {
                totalWidth
            }

            else -> {
                ((totalWidth - horizontalSpaceValue * (numItems - 1)) / numItems).toInt()
            }
        }
        // 计算一下总共有多少行,然后计算总高度
        val totalHeight = when (measurables.size % numItems != 0) {
            true -> {
                val lineCount = (measurables.size / numItems) + 1
                lineCount * finalItemWidth + (lineCount - 1) * verticalSpaceValue
            }

            else -> {
                val lineCount = measurables.size / numItems
                lineCount * finalItemWidth + (lineCount - 1) * verticalSpaceValue
            }
        }
        // 宽度就是最大宽度,高度为总高度,开始布局
        var offsetX = 0
        var offsetY = 0
        layout(width = totalWidth, height = totalHeight.toInt()) {
            measurables.forEachIndexed { index, measurable ->
                // 直接强制设置子项的宽度高度都与计算出来的平均宽度一致
                val placeable = measurable.measure(
                    Constraints(
                        minWidth = finalItemWidth,
                        maxWidth = finalItemWidth,
                        minHeight = finalItemWidth,
                        maxHeight = finalItemWidth
                    )
                )
                // 摆放
                placeable.place(offsetX, offsetY)
                if ((index + 1) % numItems == 0) {
                    // 此项是一行中的最后一列,开新的一行
                    offsetY += (finalItemWidth + verticalSpaceValue).toInt()
                    offsetX = 0
                } else {
                    // 继续下一列
                    offsetX += (finalItemWidth + horizontalSpaceValue).toInt()
                }
            }
        }
    }
}

使用案例

不放完整的使用代码了,就展示一下content里面的结构,怎么放元素
在这里插入图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值