Reputation: 541
I'm trying to implement a simple grid layout that
So I have the following code
@Composable
fun GridView(modifier: Modifier = Modifier, content: @Composable () -> Unit)
{
Layout(modifier = modifier, content = content)
{ measurables, constraints ->
// For now I'll just hard code the number of columns.
val num_cols = 2
// Determine number of rows.
val num_rows = ceil(measurables.size.toDouble() / num_cols).toInt()
val cell_width = constraints.maxWidth / num_cols
val cell_height = constraints.maxHeight / num_rows
val cell_constraints = constraints.copy(minHeight = 0, minWidth = 0,
maxHeight = cell_height, maxWidth = cell_width)
val placeables = measurables.map{ measurable -> measurable.measure(cell_constraints) }
layout(constraints.maxWidth, constraints.maxHeight)
{
placeables.forEachIndexed { i, placeable ->
// Calculate location of childs exactly.
placeable.placeRelative(x = i % num_cols * cell_width,
y = i / num_cols * cell_height)
}
}
}
}
Since the number of cells is known, the custom layout can calculate and assign the position of each cell. If I compose this with some Buttons,
GridView(Modifier.fillMaxWidth().fillMaxHeight())
{
Button(Modifier.fillMaxWidth().fillMaxHeight()) { /* ... */ }
Button(Modifier.fillMaxWidth().fillMaxHeight()) { /* ... */ }
// ...
}
I get the screenshot at the end, which is exactly what I want out of this code. However now I want to be able to say that a particular cell should be (for example) 2-wide. Is there a way to do that? In my mind the ideal interface would be something like
data class CellSpan(val col: Int, val row: Int)
GridView(Modifier.fillMaxWidth().fillMaxHeight())
{
// This cell takes 2 slots. It is 1 slot wide and 2 slot high.
CellView(modifier = Modifier.fillMaxWidth().fillMaxHeight(),
span = CellSpan(1, 2) { /* ... */ }
// This cell just takes 1 slot.
CellView(Modifier.fillMaxWidth().fillMaxHeight()) { /* ... */ }
// ...
}
Upvotes: 0
Views: 1025
Reputation: 29260
I have written an article to describe how to achieve this here.
The full code is also below, with a Preview section to show how to use it:
interface GridScope {
@Stable
fun Modifier.span(columns: Int = 1, rows: Int = 1) = this.then(
GridData(columns, rows)
)
companion object : GridScope
}
private class GridData(
val columnSpan: Int,
val rowSpan: Int,
) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any = this@GridData
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GridData
if (columnSpan != other.columnSpan) return false
if (rowSpan != other.rowSpan) return false
return true
}
override fun hashCode(): Int {
var result = columnSpan
result = 31 * result + rowSpan
return result
}
}
private val Measurable.gridData: GridData?
get() = parentData as? GridData
private val Measurable.columnSpan: Int
get() = gridData?.columnSpan ?: 1
private val Measurable.rowSpan: Int
get() = gridData?.rowSpan ?: 1
data class GridInfo(
val numChildren: Int,
val columnSpan: Int,
val rowSpan: Int,
)
@Composable
fun Grid(
columns: Int,
modifier: Modifier = Modifier,
content: @Composable GridScope.() -> Unit,
) {
check(columns > 0) { "Columns must be greater than 0" }
Layout(
content = { GridScope.content() },
modifier = modifier,
) { measurables, constraints ->
// calculate how many rows we need
val standardGrid = GridData(1, 1)
val spans = measurables.map { measurable -> measurable.gridData ?: standardGrid }
val gridInfo = calculateGridInfo(spans, columns)
val rows = gridInfo.sumOf { it.rowSpan }
// build constraints
val baseConstraints = Constraints.fixed(
width = constraints.maxWidth / columns,
height = constraints.maxHeight / rows,
)
val cellConstraints = measurables.map { measurable ->
val columnSpan = measurable.columnSpan
val rowSpan = measurable.rowSpan
Constraints.fixed(
width = baseConstraints.maxWidth * columnSpan,
height = baseConstraints.maxHeight * rowSpan
)
}
// measure children
val placeables = measurables.mapIndexed { index, measurable ->
measurable.measure(cellConstraints[index])
}
// place children
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
var x = 0
var y = 0
var childIndex = 0
gridInfo.forEach { info ->
repeat(info.numChildren) { index ->
val placeable = placeables[childIndex++]
placeable.placeRelative(
x = x,
y = y,
)
x += placeable.width
}
x = 0
y += info.rowSpan * baseConstraints.maxHeight
}
}
}
}
private fun calculateGridInfo(
spans: List<GridData>,
columns: Int,
): List<GridInfo> {
var currentColumnSpan = 0
var currentRowSpan = 0
var numChildren = 0
return buildList {
spans.forEach { span ->
val columnSpan = span.columnSpan.coerceAtMost(columns)
val rowSpan = span.rowSpan
if (currentColumnSpan + columnSpan <= columns) {
currentColumnSpan += columnSpan
currentRowSpan = max(currentRowSpan, rowSpan)
++numChildren
} else {
add(
GridInfo(
numChildren = numChildren,
columnSpan = currentColumnSpan,
rowSpan = currentRowSpan
)
)
currentColumnSpan = columnSpan
currentRowSpan = rowSpan
numChildren = 1
}
}
add(
GridInfo(
numChildren = numChildren,
columnSpan = currentColumnSpan,
rowSpan = currentRowSpan,
)
)
}
}
@Preview(widthDp = 420, heightDp = 800)
@Composable
fun PreviewGrid() {
ComposeTheme {
Surface(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background),
) {
Grid(
columns = 3,
modifier = Modifier.fillMaxSize(),
) {
Box(
modifier = Modifier
.background(Color.Red)
.span(
columns = 1,
rows = 1,
)
) {
Text(text = "1x1", modifier = Modifier.align(Alignment.Center))
}
Box(
modifier = Modifier
.background(Color.Cyan)
.span(
columns = 2,
rows = 1,
)
) {
Text(text = "2x1", modifier = Modifier.align(Alignment.Center))
}
Box(
modifier = Modifier
.background(Color.Green)
.span(
columns = 2,
rows = 1,
)
) {
Text(text = "2x1", modifier = Modifier.align(Alignment.Center))
}
Box(
modifier = Modifier
.background(Color.Red)
.span(
columns = 2,
rows = 3,
)
) {
Text(text = "2x3", modifier = Modifier.align(Alignment.Center))
}
Box(
modifier = Modifier
.background(Color.Cyan)
.span(
columns = 1,
rows = 2,
)
) {
Text(text = "1x2", modifier = Modifier.align(Alignment.Center))
}
Box(
modifier = Modifier
.background(Color.Magenta)
.span(
columns = 3,
rows = 1,
)
) {
Text(text = "3x1", modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
Upvotes: 0