Medições intrínsecas em layouts do Compose

Uma das regras do Compose é que os filhos precisam ser medidos somente uma vez. Medir os filhos duas vezes gera uma exceção de tempo de execução. No entanto, há momentos em que você precisa de algumas informações sobre os filhos antes de medi-los.

Com a medição intrínseca, é possível consultar os elementos filhos antes que eles sejam realmente medidos.

Para uma função de composição, você pode solicitar IntrinsicSize.Min ou IntrinsicSize.Max:

  • Modifier.width(IntrinsicSize.Min): qual é a largura mínima necessária para mostrar o conteúdo corretamente?
  • Modifier.width(IntrinsicSize.Max): qual é a largura máxima necessária para mostrar o conteúdo corretamente?
  • Modifier.height(IntrinsicSize.Min): qual é a altura mínima necessária para mostrar o conteúdo corretamente?
  • Modifier.height(IntrinsicSize.Max): qual é a altura máxima necessária para mostrar o conteúdo corretamente?

Por exemplo, se você solicitar a minIntrinsicHeight de um Text com restrições infinitas de width em um layout personalizado, ela vai retornar a height do Text com o texto desenhado em uma única linha.

Medições intrínsecas em ação

É possível criar um elemento combinável que mostra dois textos na tela, separados por um divisor:

Dois elementos de texto lado a lado, com um divisor vertical entre eles

Para fazer isso, use um Row com dois elementos combináveis Text que preenchem o espaço disponível e um Divider no meio. O Divider precisa ter a mesma altura que o Text mais alto e ser fino (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
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

O Divider se expande para a tela inteira, o que não é o comportamento desejado:

Dois elementos de texto lado a lado, com um divisor entre eles, mas o divisor se expande para baixo da parte inferior do texto

Isso acontece porque a Row mede cada filho individualmente, e a altura do Text não pode ser usada para limitar o Divider.

Para que o Divider preencha o espaço disponível com uma altura definida, use o modificador height(IntrinsicSize.Min).

O height(IntrinsicSize.Min) dimensiona os filhos para que a altura deles seja igual à altura intrínseca mínima. Como esse modificador é recursivo, ele consulta o minIntrinsicHeight da Row e das filhas dela.

Aplicar esse modificador ao código faz com que ele funcione como esperado:

@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
        )
        VerticalDivider(
            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")
        }
    }
}

Com a visualização:

Dois elementos de texto lado a lado, com um divisor vertical entre eles

A altura do Row é determinada da seguinte maneira:

  • A minIntrinsicHeight do elemento combinável Row é a minIntrinsicHeight máxima dos filhos dele.
  • A minIntrinsicHeight do elemento Divider é zero, porque ele não ocupa espaço se nenhuma restrição for definida.
  • O Text minIntrinsicHeight é o do texto para um width específico.
  • Portanto, a restrição de height do elemento Row se torna a minIntrinsicHeight máxima dos Texts.
  • O Divider vai expandir a height dele para a restrição da height especificada pela Row.

Medições intrínsecas nos layouts personalizados

Ao criar um modificador Layout ou layout personalizado, as medições intrínsecas são calculadas automaticamente com base nas aproximações. Portanto, os cálculos podem não estar corretos para todos os layouts. Essas APIs oferecem opções para substituir esses padrões.

Para especificar as medidas intrínsecas do Layout personalizado, modifique a minIntrinsicWidth, a minIntrinsicHeight, a maxIntrinsicWidth e a maxIntrinsicHeight da interface MeasurePolicy durante a criação dela. A substituição precisa corresponder ao que measure faz. Por exemplo, em uma coluna personalizada que empilha os filhos verticalmente e dá a cada um deles a largura total da coluna, a altura intrínseca mínima para um determinado width é a soma das alturas intrínsecas mínimas dos filhos para esse width:

@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 {
                val childConstraints = constraints.copy(minHeight = 0)
                val placeables = measurables.map { measurable ->
                    measurable.measure(childConstraints)
                }
                val width = constraints.constrainWidth(
                    placeables.maxOfOrNull { it.width } ?: 0
                )
                val height = constraints.constrainHeight(
                    placeables.sumOf { it.height }
                )
                return layout(width, height) {
                    var yPosition = 0
                    placeables.forEach { placeable ->
                        placeable.placeRelative(x = 0, y = yPosition)
                        yPosition += placeable.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                return measurables.sumOf { measurable ->
                    measurable.minIntrinsicHeight(width)
                }
            }

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

Ao criar o modificador layout personalizado, substitua os métodos relacionados na interface LayoutModifier. O cálculo intrínseco precisa corresponder a measure. Por exemplo, para um determinado height, um modificador que adiciona padding fixo em todos os lados subtrai o padding vertical antes de consultar o minIntrinsicWidth do filho e adiciona o padding horizontal ao resultado:

fun Modifier.myCustomModifier(padding: Dp = 8.dp) =
    this then object : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val placeable = measurable.measure(
                constraints.offset(-horizontal, -vertical)
            )
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {
                placeable.place(paddingPx, paddingPx)
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurable: IntrinsicMeasurable,
            height: Int
        ): Int {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val childHeight = if (height == Constraints.Infinity) {
                Constraints.Infinity
            } else {
                (height - vertical).coerceAtLeast(0)
            }
            return measurable.minIntrinsicWidth(childHeight) + horizontal
        }

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