You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中等距网格如何避免传入单元格尺寸,获取实际尺寸?

问题描述

我正在制作一个等距网格,目前通过传入单元格宽高参数来设置单元格内边距,但希望能避免传入这些参数,以最优方式使用单元格的最终尺寸。
我尝试过Stack Overflow某问题中的方案(注释代码为原实现方式),但存在额外计算、Padding must be non-negative错误,且在Android Studio预览中显示异常;使用Modifier.onGloballyPositioned也存在类似问题,请问正确的实现方式是什么?

原实现代码:

@Composable
fun <T> IsometricGrid(
    gridWidth: Int,
    gridHeight: Int,
    cellWidth: Int,
    cellHeight: Int,
    data: List<T>,
    itemContent: @Composable (Int, T) -> Unit
) {
    var width by remember { mutableStateOf(0) }
    var height by remember { mutableStateOf(0) }

    for (y in 0 until gridHeight) {
        for (x in 0 until gridWidth) {
//            val start = (y % 2 * 0.5 + x) * cellWidth
//            val top = y * cellHeight * 0.5
            val index = x * gridHeight + y

            Box(
                modifier = Modifier
                    .onSizeChanged {
                        width = it.width
                        height = it.height
                        Timber.i("$width, $height")
                    }
//                    .padding(start = start.dp, top = top.dp)
                    .padding(start = ((y % 2 * 0.5 + x) * cellWidth).dp, top = (y * height * 0.5).dp)
            ) {
                itemContent(index, data[index])
            }
        }
    }
}

使用示例:

IsometricGrid(4, 4, 100, 50, listOf<Foo>(...)) { index: Int, foo: Foo ->
    Icon(...)
}

解决方案

要实现无需传入单元格宽高的等距网格,正确做法是使用自定义Layout。直接在Composable里嵌套循环+Box的方式会导致布局计算混乱,而Layout可以统一测量所有子元素的尺寸,再根据等距规则计算每个子元素的位置,完全不需要预先指定单元格宽高。

以下是优化后的实现:

@Composable
fun <T> IsometricGrid(
    gridWidth: Int,
    gridHeight: Int,
    data: List<T>,
    modifier: Modifier = Modifier,
    itemContent: @Composable (T) -> Unit
) {
    Layout(
        modifier = modifier,
        content = {
            data.forEach { item ->
                itemContent(item)
            }
        }
    ) { measurables, constraints ->
        // 测量第一个子元素获取单元格真实尺寸(假设所有子元素尺寸一致)
        val cellMeasurable = measurables.firstOrNull() ?: return@Layout
        val cellPlaceable = cellMeasurable.measure(constraints)
        val cellWidth = cellPlaceable.width.toFloat()
        val cellHeight = cellPlaceable.height.toFloat()

        // 计算整个网格的宽高
        val gridTotalWidth = (gridWidth * cellWidth) + (cellWidth / 2f)
        val gridTotalHeight = (gridHeight * cellHeight / 2f) + cellHeight / 2f

        // 布局所有子元素
        layout(gridTotalWidth.toInt(), gridTotalHeight.toInt()) {
            data.forEachIndexed { index, _ ->
                val y = index / gridWidth
                val x = index % gridWidth

                val placeable = measurables[index].measure(constraints)

                // 等距网格位置计算:奇数行偏移半个单元格宽度,每行垂直偏移半个单元格高度
                val offsetX = (x * cellWidth) + if (y % 2 == 1) cellWidth / 2f else 0f
                val offsetY = y * cellHeight / 2f

                placeable.placeRelative(offsetX.toInt(), offsetY.toInt())
            }
        }
    }
}

关键说明

  • 自定义Layout的优势:

    • 统一测量子元素,直接获取真实单元格尺寸,无需外部传参
    • 避免onSizeChanged/onGloballyPositioned带来的状态更新循环和预览异常
    • 彻底解决Padding must be non-negative错误,所有位置计算基于真实测量值,不会出现负数
  • 等距逻辑:
    奇数行元素向右偏移半个单元格宽度形成交错效果,每行垂直偏移为上一行偏移加半个单元格高度,符合等距网格视觉特性

  • 新使用方式:
    无需传入单元格宽高,直接使用:

IsometricGrid(4, 4, listOf<Foo>(...)) { foo: Foo ->
    Icon(...) // Icon的尺寸即为单元格实际尺寸
}

额外优化点

  • 如果子元素尺寸不一致,可修改测量逻辑,取所有子元素的最大宽高作为单元格尺寸
  • 可添加padding、spacing等参数增强网格灵活性

内容的提问来源于stack exchange,提问作者Psijic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 22:50:33