Jetpack Compose: building a generic grid canvas

As promised in my last article, here I am back again with the grid canvas we used to replicate IntelliJ IDEA’s splash screen. This time I will be explaining how to implement some additional features.

Content alignment

We want our composable to have the ability to set where the cells will be placed relative to the canvas bounds, or how the space between the grid and the canvas limits will be distributed.

fun Size.toIntSize(): IntSize = IntSize(width.toInt(), height.toInt())val alignOffset = contentAlignment.align(
Size(
columns * cellSize,
rows * cellSize
).toIntSize(), size.toIntSize(), layoutDirection
)
translate(alignOffset.x.toFloat(), alignOffset.y.toFloat()) {
// Draw the cells
}
3 rows based grid with center aligned content

Additional sizing factors

In the original implementation of this grid based canvas, the sizing was based on a fixed number of rows and columns. Now we are going to make it support different ways of sizing.

class CanvasData(val rows: Int, val columns: Int, val cellSize: Float)
sealed class Sizing {
abstract fun calculateCanvasData(canvasSize: Size): CanvasData
}
sealed class Sizing {
class Rows(private val rows: Int) : Sizing()
class Columns(private val columns: Int) : Sizing()
class RowsAndColumns(private val rows: Int, private val columns: Int) : Sizing()
class CellSize(private val size: Float) : Sizing()
}

Fixed rows

Here the number of rows is provided, and the columns are calculated based on the canvas size:

class Rows(private val rows: Int) : Sizing() {
override fun calculateCanvasData(canvasSize: Size): CanvasData {
val cellWidth = min(canvasSize.width, canvasSize.height / rows)
val columns = (canvasSize.width / cellWidth).toInt()
return CanvasData(rows, columns, cellWidth)
}
}

Fixed columns

This is the same as the previous one but based on the columns:

class Columns(private val rows: Int) : Sizing() {
override fun calculateCanvasData(canvasSize: Size): CanvasData {
val cellHeight = min(canvasSize.height, canvasSize.width / columns)
val rows = (canvasSize.height / cellHeight).toInt()
return CanvasData(rows, columns, cellHeight)
}
}

Fixed rows and columns

In this one both the number of rows and columns are provided and the cell size is calculated:

class RowsAndColumns(private val rows: Int, private val columns: Int) : Sizing() {
override fun calculateCanvasData(canvasSize: Size): CanvasData {
val canvasRatio = canvasSize.width / canvasSize.height
val cellSize = if (columns.toFloat() / rows < canvasRatio) {
canvasSize.height / rows
} else {
canvasSize.width / columns
}
return CanvasData(rows, columns, cellSize)
}
}

Fixed cell size

In this case the number of rows and columns that fit on the canvas are calculated given the cell size:

class CellSize(private val size: Float) : Sizing() {
override fun calculateCanvasData(canvasSize: Size): CanvasData =
CanvasData(
rows = (canvasSize.height / size).toInt(),
columns = (canvasSize.width / size).toInt(),
cellSize = size
)
}

Cells aspect ratio

Another nice to have feature is the possibility to set the cell’s aspect ratio (instead of forcing to be squares). The aspect ratio is the relation between the width and height.

class CanvasData(val rows: Int, val columns: Int, val cellSize: Size)
class Rows(private val rows: Int, private val sizeRatio: Float) : Sizing() {
override fun calculateCanvasData(canvasSize: Size): CanvasData {
val cellWidth = min(canvasSize.width, canvasSize.height / rows * sizeRatio)
val cellHeight = cellWidth / sizeRatio
val columns = (canvasSize.width / cellWidth).toInt()
return CanvasData(rows, columns, Size(cellWidth, cellHeight))
}
}
Grid canvas with a size ratio of 2

Making it reusable

To make this grid canvas as much reusable as possible it must be agnostic from the details of what or how each cell will draw. With all the above, our generic composable should look like:

@Composable
fun GridCanvas(
sizing: Sizing,
onDrawCell: DrawScope.(row: Int, column: Int, size: Size) -> Unit,
modifier: Modifier,
contentAlignment: Alignment = Alignment.TopStart,
) {
}
GridCanvas(
sizing = Sizing.Rows(rows = 1, sizeRatio = 1f),
onDrawCell = { row, column, cellSize ->
// TODO Draw cell content
},
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
)

Conclusion

Adding these extra features to our grid canvas make it to be more reusable and cover additional use-cases. Do you think it may deserve to make a library for this? Let me know in the comments above :)

Impure developer and functional programming enthusiast

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store