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 que pode ser composta, você pode solicitar intrinsicWidth ou intrinsicHeight:

  • (min|max)IntrinsicWidth: considerando essa altura, quais são as larguras mínima e máxima para que o conteúdo seja pintado corretamente?
  • (min|max)IntrinsicHeight: considerando essa largura, quais são as alturas mínima e máxima para que o conteúdo seja pintado corretamente?

Por exemplo, se você solicitar minIntrinsicHeight de um Text com width infinita, ela retornará a height do Text como se o texto tivesse sido desenhado em uma única linha.

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

Imagine que queremos criar um elemento que pode ser composto que exibe dois textos na tela, separados por um divisor como este:

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

Como podemos fazer isso? Podemos ter uma Row com dois Texts que se expandem o máximo possível e um Divider no meio. O divisor precisa ter a mesma altura que o Text mais alto e ser fino (width = 1.dp).

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    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
        )
    }
}

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

Na visualização, veremos que o divisor é expandido para a tela inteira e esse não é o resultado esperado:

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 Row mede cada filho individualmente, e a altura de Text não pode ser usada para limitar o Divider. O objetivo é que Divider preencha o espaço disponível com uma altura definida. Para isso, podemos usar o modificador height(IntrinsicSize.Min)

height(IntrinsicSize.Min) dimensiona os filhos, para que a altura deles seja igual à altura intrínseca mínima. Por ser recursivo, ele consultará a minIntrinsicHeight da Row e das filhas dela.

Aplicando isso ao código, ele funcionará como esperado:

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    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
        )
    }
}

Com a visualização:

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

A minIntrinsicHeight que pode ser composta da Row será a minIntrinsicHeight máxima das filhas dela. O minIntrinsicHeight do elemento Divider é 0 porque não ocupa espaço quando nenhuma restrição é estabelecida. A minIntrinsicHeight do Text será igual ao do texto que recebeu uma width específica. Portanto, a restrição de height do elemento Row será a minIntrinsicHeight máxima dos Texts. O Divider expandirá a height dele para a restrição de 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 medições intrínsecas do Layout personalizado, substitua minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth e maxIntrinsicHeight da interface MeasurePolicy ao criá-lo.

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    return 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
        ) = {
            // Logic here
        }

        // 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.

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.
})

Saiba mais

Saiba mais sobre medições intrínsecas na seção Medições intrínsecas no codelab de Layouts no Jetpack Compose.