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
相关产品推荐
相关产品推荐

