On this page
With Views, a custom component meant subclassing View, overriding onMeasure, onLayout and onDraw, and declaring XML attributes. In Compose, layout is one function that measures children and places them, and drawing is a lambda with a canvas.
How Compose layout works
Example 1: A simple wrapping tag layout
Compose already provides FlowRow for this. Building a simplified version yourself is the best way to learn the Layout API:
@Composable
fun TagWrapLayout(
modifier: Modifier = Modifier,
horizontalGap: Dp = 8.dp,
verticalGap: Dp = 8.dp,
content: @Composable () -> Unit,
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val hGap = horizontalGap.roundToPx()
val vGap = verticalGap.roundToPx()
// Children may be any width up to ours; never force a minimum on them.
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val placeables = measurables.map { it.measure(childConstraints) }
// Compute positions row by row.
val positions = ArrayList<IntOffset>(placeables.size)
var x = 0; var y = 0; var rowHeight = 0
placeables.forEach { p ->
if (x > 0 && x + p.width > constraints.maxWidth) { // wrap to next row
x = 0; y += rowHeight + vGap; rowHeight = 0
}
positions += IntOffset(x, y)
x += p.width + hGap
rowHeight = maxOf(rowHeight, p.height)
}
val height = (y + rowHeight).coerceIn(constraints.minHeight, constraints.maxHeight)
layout(width = constraints.maxWidth, height = height) {
placeables.forEachIndexed { i, p -> p.place(positions[i]) }
}
}
}This assumes a bounded maximum width, which is the normal case inside a screen. Inside a horizontally scrolling parent, maxWidth would be infinite, and you'd need to handle that.
The layout modifier for a single element
fun Modifier.firstBaselineToTop(distance: Dp) = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val baseline = placeable[FirstBaseline]
check(baseline != AlignmentLine.Unspecified)
val y = distance.roundToPx() - baseline
layout(placeable.width, placeable.height + y) { placeable.placeRelative(0, y) }
}Drawing: Canvas, drawBehind and drawWithCache
Canvas(modifier) { โฆ }: a composable that is purely a drawing surface.Modifier.drawBehind: draw behind a composable's content, for example custom backgrounds.Modifier.drawWithCache: create objects (paths, brushes) once and reuse them until the size or state changes.
Example 2: An accessible progress ring chart
@Composable
fun ProgressRing(
progress: Float, // 0f..1f
label: String,
modifier: Modifier = Modifier,
strokeWidth: Dp = 12.dp,
trackColor: Color = MaterialTheme.colorScheme.surfaceVariant,
progressColor: Color = MaterialTheme.colorScheme.primary,
) {
val animated by animateFloatAsState(progress.coerceIn(0f, 1f), label = "ring")
Box(
modifier = modifier
.size(120.dp)
.semantics(mergeDescendants = true) {
contentDescription = "$label: ${(progress * 100).roundToInt()} percent"
progressBarRangeInfo = ProgressBarRangeInfo(progress, 0f..1f)
}
.drawWithCache {
val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
val inset = stroke.width / 2
val arcSize = Size(size.width - stroke.width, size.height - stroke.width)
onDrawBehind {
drawArc(trackColor, 0f, 360f, false, Offset(inset, inset), arcSize, style = stroke)
drawArc(progressColor, -90f, 360f * animated, false, Offset(inset, inset), arcSize, style = stroke)
}
},
contentAlignment = Alignment.Center,
) {
Text("${(animated * 100).roundToInt()}%", style = MaterialTheme.typography.titleLarge)
}
}Because animated is read inside the draw lambda, each animation frame only redraws, with no recomposition of the ring. The semantics block makes the chart understandable to TalkBack users, which custom drawing would otherwise lose.
When to go custom
Charts and gauges
Lightweight bar, line or ring charts drawn with Canvas, avoiding a heavy charting library for simple needs.
Special arrangements
Timelines, staggered badges and overlapping avatars that Row and Column can't express cleanly.
Decorations
Dashed borders, gradient strokes and ticket notches with drawBehind or custom Shapes.
Common mistakes
- Measuring a child twice in one layout pass, which isn't allowed. Use intrinsic measurements or
SubcomposeLayoutif you truly need it. - Allocating Paths and Brushes in
drawBehindevery frame. UsedrawWithCache. - Ignoring constraints, for example returning a size larger than
maxWidth. - Forgetting semantics, which makes custom components invisible to screen readers.
- Ignoring right-to-left layouts. Use
placeRelativefor direction-aware placement.
Frequently asked questions
When should I use SubcomposeLayout?
When a child's content depends on another child's measured size, as in BoxWithConstraints or some table layouts. It's more expensive, so prefer Layout when possible.
Can I use Android's Canvas APIs?
Yes, via drawIntoCanvas { it.nativeCanvas } for things like advanced text drawing, though Compose's DrawScope covers most needs.
How do I handle touches on a custom drawing?
Use Modifier.pointerInput with gesture detectors such as detectTapGestures, and map touch positions to your drawn shapes.
Conclusion
Custom Compose components come down to two tools: Layout (constraints down, sizes up, then place) and drawing modifiers (Canvas, drawBehind, drawWithCache). Read animated state in the draw phase, cache drawing objects, and always add semantics.
Next step: lock in your UI's correctness with Compose UI Testing and Screenshot Testing.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.