Custom Layouts and Graphics in Jetpack Compose
Mircea Ioan Soit
Senior Android Developer | Business Owner | Contractor | Team Builder | Founder of IC & Codertal
Part of the series "Android Development Series by Mircea Ioan Soit"
Jetpack Compose makes it easy to create custom layouts and graphics using its powerful, declarative API. While Compose provides a wide range of built-in layouts like Row, Column, and Box, there are cases where you need more control over positioning and drawing elements. In this article, we’ll explore how to build custom layouts and handle complex graphics in Jetpack Compose.
Why Custom Layouts Matter
Standard layouts cover most use cases, but custom layouts are essential when:
Building a Custom Layout
You can create a custom layout using the Layout composable. The Layout composable allows you to define how child elements are measured and positioned.
Here's a simple example of a custom FlowLayout that arranges elements horizontally and wraps them to the next row if needed:
@Composable
fun FlowLayout(
modifier: Modifier = Modifier,
spacing: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val rowSpacing = spacing.roundToPx()
var xPosition = 0
var yPosition = 0
var rowHeight = 0
placeables.forEach { placeable ->
if (xPosition + placeable.width > constraints.maxWidth) {
xPosition = 0
yPosition += rowHeight + rowSpacing
rowHeight = 0
}
rowHeight = maxOf(rowHeight, placeable.height)
xPosition += placeable.width + rowSpacing
}
layout(constraints.maxWidth, yPosition + rowHeight) {
xPosition = 0
yPosition = 0
rowHeight = 0
placeables.forEach { placeable ->
if (xPosition + placeable.width > constraints.maxWidth) {
xPosition = 0
yPosition += rowHeight + rowSpacing
rowHeight = 0
}
placeable.place(x = xPosition, y = yPosition)
rowHeight = maxOf(rowHeight, placeable.height)
xPosition += placeable.width + rowSpacing
}
}
}
}
In this example:
Using the Custom Layout
You can use the custom FlowLayout like any other composable:
FlowLayout {
repeat(10) {
Box(
modifier = Modifier
.size(80.dp)
.background(Color.Blue)
)
}
}
Creating a Circular Layout
To create a circular layout, you can calculate the position of each element based on the angle and radius.
@Composable
fun CircularLayout(
modifier: Modifier = Modifier,
radius: Dp = 100.dp,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val centerX = constraints.maxWidth / 2
val centerY = constraints.maxHeight / 2
val radiusPx = radius.roundToPx()
val angleStep = 360f / placeables.size
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEachIndexed { index, placeable ->
val angle = Math.toRadians((angleStep * index).toDouble())
val x = (centerX + radiusPx * cos(angle) - placeable.width / 2).toInt()
val y = (centerY + radiusPx * sin(angle) - placeable.height / 2).toInt()
placeable.place(x, y)
}
}
}
}
In this example:
Using the Circular Layout
You can use the CircularLayout like this:
CircularLayout(radius = 100.dp) {
repeat(6) {
Box(
modifier = Modifier
.size(40.dp)
.background(Color.Red)
)
}
}
Custom Graphics with Canvas
You can combine custom layouts with Canvas to create complex graphics.
Here's an example of a simple bar chart using Canvas:
@Composable
fun BarChart(values: List<Float>) {
Canvas(modifier = Modifier
.fillMaxWidth()
.height(150.dp)
) {
val barWidth = size.width / values.size
values.forEachIndexed { index, value ->
drawRect(
color = Color.Green,
topLeft = Offset(index * barWidth, size.height - value),
size = Size(barWidth - 4.dp.toPx(), value)
)
}
}
}
In this example:
Handling Touch Events in Custom Layouts
You can use Modifier.pointerInput to add interactivity to custom layouts:
@Composable
fun InteractiveLayout() {
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
offset += dragAmount
}
}
) {
Box(
modifier = Modifier
.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
.size(50.dp)
.background(Color.Magenta)
)
}
}
Performance Considerations
Best Practices for Custom Layouts
Conclusion
Custom layouts and graphics in Jetpack Compose give you the flexibility to create unique designs and interactive UIs. By understanding Layout, Canvas, and PointerInput, you can craft custom experiences that go beyond standard composables.
Key Takeaways: