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

Jetpack Compose 固定列数全屏网格布局如何实现单元格合并?

Compose自定义网格布局实现单元格跨行跨列方案

核心实现思路

我们需要通过Compose的ParentData机制传递子元素的合并规则,同时调整布局的测量、占位逻辑,适配带合并属性的单元格:

  • 为子组件添加自定义ParentData承载跨列、跨行参数
  • 替换原来简单的行数计算逻辑,遍历所有单元格的span属性计算总行数,同时标记网格的占用状态避免重叠
  • 针对每个子元素的span值动态生成测量约束,计算实际占位宽高和位置

完整实现代码

1. 定义基础配置类与Modifier扩展

首先定义合并规则类,以及Modifier扩展方法用来给子元素设置合并属性:

// 单元格合并规则:col为跨列数,row为跨行数,默认均为1
data class CellSpan(val col: Int = 1, val row: Int = 1)

// 自定义ParentData用来传递子元素的合并配置
internal class GridCellParentData(var span: CellSpan = CellSpan(1, 1)) : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return this@GridCellParentData
    }
}

// Modifier扩展,给子元素设置合并规则
fun Modifier.cellSpan(span: CellSpan): Modifier = this.then(GridCellParentData(span))

2. 调整GridView自定义Layout逻辑

@Composable
fun GridView(
    modifier: Modifier = Modifier,
    columns: Int = 2, // 固定列数,默认2列,可自定义
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 固定列数,计算单格基础宽高
        val cellBaseWidth = constraints.maxWidth / columns
        // 第一步:先遍历所有子元素,计算网格总行数,同时记录每个子元素的起始位置
        val cellInfos = mutableListOf<Triple<Int, Int, CellSpan>>() // 起始行、起始列、合并规则
        // 网格占用标记:行索引 -> 该行已占用的列集合
        val occupiedGrid = mutableMapOf<Int, MutableSet<Int>>()
        var maxRow = 0

        measurables.forEach { measurable ->
            // 读取子元素的合并规则,默认1行1列
            val span = (measurable.parentData as? GridCellParentData)?.span ?: CellSpan(1, 1)
            // 找第一个空闲的位置放置当前单元格
            var startRow = 0
            var startCol = 0
            var found = false
            // 遍历行找可用位置
            while (!found) {
                val occupiedCols = occupiedGrid.getOrPut(startRow) { mutableSetOf() }
                // 找当前行连续可用的列,满足span.col的宽度
                var consecutiveFree = 0
                startCol = 0
                for (col in 0 until columns) {
                    if (col !in occupiedCols) {
                        consecutiveFree++
                        if (consecutiveFree == span.col) {
                            startCol = col - span.col + 1
                            found = true
                            break
                        }
                    } else {
                        consecutiveFree = 0
                    }
                }
                if (!found) startRow++
            }
            // 标记当前单元格占用的所有网格位置
            for (r in startRow until startRow + span.row) {
                val cols = occupiedGrid.getOrPut(r) { mutableSetOf() }
                for (c in startCol until startCol + span.col) {
                    cols.add(c)
                }
            }
            cellInfos.add(Triple(startRow, startCol, span))
            // 更新最大行数
            maxRow = maxOf(maxRow, startRow + span.row)
        }

        // 计算单格基础高度
        val cellBaseHeight = constraints.maxHeight / maxRow

        // 第二步:测量所有子元素
        val placeables = measurables.mapIndexed { index, measurable ->
            val (_, _, span) = cellInfos[index]
            val cellConstraints = constraints.copy(
                minWidth = 0,
                minHeight = 0,
                maxWidth = cellBaseWidth * span.col,
                maxHeight = cellBaseHeight * span.row
            )
            measurable.measure(cellConstraints)
        }

        // 第三步:放置子元素
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEachIndexed { index, placeable ->
                val (startRow, startCol, _) = cellInfos[index]
                placeable.placeRelative(
                    x = startCol * cellBaseWidth,
                    y = startRow * cellBaseHeight
                )
            }
        }
    }
}

使用示例

和预期的调用方式基本一致,只需要通过Modifier设置span属性即可:

// 撑满全屏的3列网格示例
GridView(
    modifier = Modifier.fillMaxSize(),
    columns = 3
) {
    // 跨1列、2行的按钮
    Button(
        modifier = Modifier
            .fillMaxSize()
            .cellSpan(CellSpan(col = 1, row = 2))
    ) {
        Text("跨2行")
    }
    // 跨2列、1行的按钮
    Button(
        modifier = Modifier
            .fillMaxSize()
            .cellSpan(CellSpan(col = 2, row = 1))
    ) {
        Text("跨2列")
    }
    // 普通1x1单元格
    Button(Modifier.fillMaxSize()) { Text("普通1") }
    Button(Modifier.fillMaxSize()) { Text("普通2") }
    Button(Modifier.fillMaxSize()) { Text("普通3") }
}

效果说明

该实现完全满足需求:

  • 列数固定可配置,网格默认撑满父布局所有空间
  • 单元格无间隙自动填充,不会出现重叠
  • 支持任意合法的跨列、跨行合并规则,静态单元格无需动态渲染逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 04:54:01