Intrinsic measurements in Compose layouts

One of the rules of Compose is that you should only measure your children once; measuring children twice throws a runtime exception. However, there are times when you need some information about your children before measuring them.

Intrinsics lets you query children before they're actually measured.

To a composable, you can ask for its intrinsicWidth or intrinsicHeight:

  • (min|max)IntrinsicWidth: Given this height, what's the minimum/maximum width you can paint your content properly?
  • (min|max)IntrinsicHeight: Given this width, what's the minimum/maximum height you can paint your content properly?

For example, if you ask the minIntrinsicHeight of a Text with infinite width, it'll return the height of the Text as if the text was drawn in a single line.

Intrinsics in action

Imagine that we want to create a composable that displays two texts on the screen separated by a divider like this:

Two text elements side by side, with a vertical divider between them

How can we do this? We can have a Row with two Texts inside that expands as much as they can and a Divider in the middle. We want the Divider to be as tall as the tallest Text and thin (width = 1.dp).

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

If we preview this, we see that the Divider expands to the whole screen and that's not what we want:

Two text elements side by side, with a divider between them, but the divider stretches down below the bottom of the text

This happens because Row measures each child individually and the height of Text cannot be used to constraint the Divider. We want the Divider to fill the available space with a given height. For that, we can use the height(IntrinsicSize.Min) modifier .

height(IntrinsicSize.Min) sizes its children being forced to be as tall as their minimum intrinsic height. As it's recursive, it'll query Row and its children minIntrinsicHeight.

Applying that to our code, it'll work as expected:

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

// @Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

With preview:

Two text elements side by side, with a vertical divider between them

The Row composable's minIntrinsicHeight will be the maximum minIntrinsicHeight of its children. The Divider element's minIntrinsicHeight is 0 as it doesn't occupy space if no constraints are given; the Text minIntrinsicHeight will be that of the text given a specific width. Therefore, the Row element's height constraint will be the max minIntrinsicHeight of the Texts. Divider will then expand its height to the height constraint given by the Row.

Intrinsics in your custom layouts

When creating a custom Layout or layout modifier, intrinsic measurements are calculated automatically based on approximations. Therefore, the calculations might not be correct for all layouts. These APIs offer options to override these defaults.

To specify the intrinsics measurements of your custom Layout, override the minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, and maxIntrinsicHeight of the MeasurePolicy interface when creating it.

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // Measure and layout here
                // ...
            }

            override fun IntrinsicMeasureScope.minIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ): Int {
                // Logic here
                // ...
            }

            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )
}

When creating your custom layout modifier, override the related methods in the LayoutModifier interface.

fun Modifier.myCustomModifier(/* ... */) = this then object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
        // ...
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int {
        // Logic here
        // ...
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
}