Phase 5 · Advanced ComposeModule 27~30 min read

Custom Layouts & Canvas

Go beyond built-ins with the Layout composable and custom drawing on the Canvas.

What you'll learn

Column, Row, and Box handle 95% of layouts — but when you need something they can't express, or a bespoke graphic like a progress ring, Compose lets you drop down a level. This module demystifies how layout actually works and shows you how to draw anything with the Canvas.

By the end you'll be able to:

  • Explain Compose's three rendering phases
  • Write a custom layout with the Layout composable
  • Measure and place children yourself
  • Draw custom graphics with Canvas and drawBehind

The three phases

Every frame, Compose runs three phases. Understanding them is the key to this whole module:

  1. Composition — what to show. Your composables run and build the UI tree.
  2. Layout — where and how big. Each node is measured, then placed.
  3. Drawing — how it looks. Each node paints itself onto the canvas.

So far you've worked in composition. Custom layouts hook into the layout phase; Canvas hooks into the drawing phase.

The Layout composable

The Layout composable lets you define your own measuring and positioning logic. It hands you the children as measurables and the space you have as constraints, and you return your size and where each child goes. Here's a from-scratch vertical stack (a simplified Column):

VerticalStack.kt
@Composable
fun VerticalStack(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(modifier = modifier, content = content) { measurables, constraints ->
        // 1) Measure each child within the constraints.
        val placeables = measurables.map { it.measure(constraints) }

        // 2) Decide our own size.
        val width = placeables.maxOf { it.width }
        val height = placeables.sumOf { it.height }

        // 3) Place each child.
        layout(width, height) {
            var y = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = y)
                y += placeable.height
            }
        }
    }
}

Measure & place

The custom-layout contract is always the same three steps:

  • Measure each child exactly once by calling measure(constraints), which returns a Placeable (the child at its chosen size).
  • Size yourself based on those placeables.
  • Place each placeable at an (x, y) inside the layout(w, h) block with placeRelative (which respects right-to-left languages).

Watch out

Measure each child exactly once — measuring twice throws an exception, and it's what keeps Compose layout fast (single-pass). This is the one hard rule of custom layouts.

Drawing with Canvas

For custom graphics, Canvas gives you a DrawScope with primitives — drawCircle, drawRect, drawArc, drawLine, drawPath. A circular progress indicator is a couple of arcs:

ProgressRing.kt
Canvas(modifier = Modifier.size(120.dp)) {
    // 'this' is a DrawScope: size, center, and draw* functions are available.
    drawCircle(color = Color.LightGray, style = Stroke(width = 12f))
    drawArc(
        color = Color(0xFF6366F1),
        startAngle = -90f,
        sweepAngle = 360f * progress,     // e.g. 0.7 = 70%
        useCenter = false,
        style = Stroke(width = 12f, cap = StrokeCap.Round),
    )
}
9:41
70%
Canvas — how this renders on a device

For lighter needs, the Modifier.drawBehind and drawWithContent modifiers let you draw around an existing composable without a separate Canvas — great for a highlight behind text:

Highlight.kt
Text(
    "Highlighted",
    modifier = Modifier
        .drawBehind {                     // draw underneath the text, cheaply
            drawRoundRect(Color(0x336366F1), cornerRadius = CornerRadius(8f))
        }
        .padding(8.dp),
)

Tip

Reach for custom layouts and Canvas last. Try to compose from built-ins first — they're simpler and battle-tested. But when a design genuinely needs a flow layout, a radial menu, or a custom chart, it's wonderful that Compose lets you build it in the same language, no escape hatch to a different system.

Recap & quick check

Key takeaways

  • Compose renders in three phases: composition (what), layout (where/size), drawing (how it looks).
  • The Layout composable lets you define custom measuring and placement.
  • The contract: measure each child once → decide your size → place each child with placeRelative.
  • Measure each child exactly once — measuring twice throws and breaks single-pass layout.
  • Canvas (DrawScope) draws custom graphics; Modifier.drawBehind draws around an existing composable.

Quick check

1. What are Compose's three phases, in order?

2. In a custom Layout, how many times may you measure each child?

3. What does measure(constraints) return?

4. Which draws custom graphics like a progress ring?

5. When should you build a custom layout?

You can build any UI Compose can render. To finish Phase 5, let's make sure it all runs fast — performance and best practices.