Alignment lines in Jetpack Compose

The Compose layout model lets you use AlignmentLine to create custom alignment lines that can be used by parent layouts to align and position their children. For example, Row can use its children's custom alignment lines to align them.

When a layout provides a value for a particular AlignmentLine, the layout's parents can read this value after measuring, using the Placeable.get operator on the corresponding Placeable instance. Based on the position of the AlignmentLine, the parents can then decide the positioning of the children.

Some composables in Compose already come with alignment lines. For example, the BasicText composable exposes the FirstBaseline and LastBaseline alignment lines.

In the example below, a custom LayoutModifier called firstBaselineToTop reads the FirstBaseline to add padding to the Text starting from its first baseline.

Figure 1. Shows the difference between adding normal padding to an element, and applying padding to a Text element's baseline.

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    // Measure the composable
   val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

In order to read the FirstBaseline in the example, placeable [FirstBaseline] is used in the measurement phase.

Creating custom alignment lines

When creating a custom Layout composable or a custom LayoutModifier, you can provide custom alignment lines so that other parent composables can use them to align and position their children accordingly.

The following example shows a custom BarChart composable that exposes two alignment lines, MaxChartValue and MinChartValue, so that other composables can align to the maximum and minimum data value of the chart. Two text elements, Max and Min, have been aligned to the center of the custom alignment lines.

Figure 2. BarChart composable with Text aligned to the maximum and minimum data value.

Custom alignment lines are defined as top level variables in your project.

import kotlin.math.max
import kotlin.math.min
import androidx.compose.ui.layout.HorizontalAlignmentLine

/**
 * AlignmentLine defined by the maximum data value in a [BarChart]
 */
val MaxChartValue = HorizontalAlignmentLine(merger = { old, new -> min(old, new) })

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
val MinChartValue = HorizontalAlignmentLine(merger = { old, new -> max(old, new) })

The custom alignment lines to create our example are of type HorizontalAlignmentLine, as they're used to align children vertically. A merge policy is passed as a parameter in case multiple layouts provide a value for these alignment lines. As the Compose layout system coordinates and the Canvas coordinates represent [0, 0], the top left corner and the x and y axis are positive downwards, so the MaxChartValue value will always be smaller than MinChartValue. Therefore, the merger policy is min for the maximum chart data value baseline, and max for the minimum chart data value baseline.

When creating a custom Layout or LayoutModifier, specify custom alignment lines in the MeasureScope.layout method, which takes an alignmentLines: Map<AlignmentLine, Int> parameter.

@Composable
fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier
) {
    var maxValueBaseline by remember { mutableStateOf(Float.MAX_VALUE) }
    var minValueBaseline by remember { mutableStateOf(Float.MIN_VALUE) }

    Layout(
        modifier = modifier,
        content = {
            // ... Logic to draw the chart in a Canvas ...
            // maxValueBaseline and minValueBaseline are updated here
        }
    ) { measurables, constraints ->
        val placeable = measurables[0].measure(constraints)
        layout(
            width = constraints.maxWidth,
            height = constraints.maxHeight,
            // Custom AlignmentLines are set here. These are propagated
            // to direct and indirect parent composables.
            alignmentLines = mapOf(
                MinChartValue to minValueBaseline.roundToInt(),
                MaxChartValue to maxValueBaseline.roundToInt()
            )
        ) {
            placeable.placeRelative(0, 0)
        }
    }
}

Direct and indirect parents of this composable can consume the alignment lines. The following composable creates a custom layout that takes as a parameter two Text slots and data points, and aligns the two texts to the maximum and minimum chart data values. The preview of this composable is what's shown in Figure 2.

@Composable
fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier
) {
    Layout(
        content = {
            maxText()
            minText()
            // Set a fixed size to make the example easier to follow
            BarChart(dataPoints, Modifier.size(200.dp))
        },
        modifier = modifier
    ) { measurables, constraints ->
        check(measurables.size == 3)
        val placeables = measurables.map {
            it.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        val maxTextPlaceable = placeables[0]
        val minTextPlaceable = placeables[1]
        val barChartPlaceable = placeables[2]

        // Obtain the alignment lines from BarChart to position the Text
        val minValueBaseline = barChartPlaceable[MinChartValue]
        val maxValueBaseline = barChartPlaceable[MaxChartValue]
        layout(constraints.maxWidth, constraints.maxHeight) {
            maxTextPlaceable.placeRelative(
                x = 0,
                y = maxValueBaseline - (maxTextPlaceable.height / 2)
            )
            minTextPlaceable.placeRelative(
                x = 0,
                y = minValueBaseline - (minTextPlaceable.height / 2)
            )
            barChartPlaceable.placeRelative(
                x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
                y = 0
            )
        }
    }
}


@Preview
@Composable
fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}