Medidas intrínsecas en los diseños de Compose

Una de las reglas de Compose es que solo debes medir tus elementos secundarios una vez. Si lo haces dos veces, se genera una excepción de tiempo de ejecución. Sin embargo, hay momentos en los que necesitas información sobre tus elementos secundarios antes de medirlos.

Los elementos intrínsecos te permiten realizar consultas a los elementos secundarios antes de que se midan realmente.

Para un elemento componible, puedes solicitar su intrinsicWidth o intrinsicHeight:

  • (min|max)IntrinsicWidth: Con este ancho, ¿cuál es el ancho mínimo y máximo con el que puedes pintar el contenido de manera correcta?
  • (min|max)IntrinsicHeight: Con esta altura, ¿cuál es la altura mínima o máxima con la que puedes pintar el contenido de manera correcta?

Por ejemplo, si solicitas el minIntrinsicHeight de un Text con height infinito, se mostrará el height del Text como si el texto se hubiera dibujado en una sola línea.

Funciones intrínsecas en acción

Imagina que queremos crear un elemento componible que muestre dos textos en la pantalla separados por un divisor como este:

Dos elementos de texto, uno al lado del otro, con un divisor vertical entre ellos

¿Cómo podemos hacer esto? Podemos tener un objeto Row con dos Text que se expandan tanto como sea posible y un Divider en el medio. Queremos que el Divider sea tan alto como el Text más alto y delgado (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
        )
    }
}

En la vista previa, vemos que Divider se expande a toda la pantalla, y eso no es lo que queremos:

Dos elementos de texto, uno al lado del otro, con un divisor entre ellos, pero el divisor se expande debajo de la parte inferior del texto

Esto ocurre porque Row mide cada elemento secundario de forma individual, y la altura de Text no se puede usar para restringir Divider. Queremos que el Divider ocupe el espacio disponible con una altura determinada. Para eso, podemos usar el modificador height(IntrinsicSize.Min).

height(IntrinsicSize.Min) ajusta su tamaño a los elementos secundarios para que sean tan altos como su altura mínima intrínseca. Como es recurrente, realizará consultas a Row y sus elementos secundarios minIntrinsicHeight.

Cuando lo apliquemos a nuestro código, funcionará según lo 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
        )
        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")
        }
    }
}

Con vista previa:

Dos elementos de texto, uno al lado del otro, con un divisor vertical entre ellos

El elemento componible minIntrinsicHeight de la Row será la minIntrinsicHeight máxima de sus elementos secundarios. El elemento minIntrinsicHeight de Divider es 0, ya que no ocupa espacio si no se le aplican restricciones. El minIntrinsicHeight de Text será el del texto según un width específico. Por lo tanto, la restricción height del elemento Row será la minIntrinsicHeight máxima de los Text. Luego, Divider expandirá su height a la restricción height proporcionada por Row.

Funciones intrínsecas en tus diseños personalizados

Cuando se crea un modificador Layout o layout personalizado, las mediciones intrínsecas se calculan automáticamente en función de aproximaciones. Por lo tanto, es posible que los cálculos no sean correctos para todos los diseños. Estas APIs ofrecen opciones para anular estos valores predeterminados.

Para especificar las mediciones intrínsecas de tu Layout personalizado, anula minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth y maxIntrinsicHeight de MeasurePolicy cuando la creas.

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

Cuando crees el modificador layout personalizado, anula los métodos relacionados en la interfaz 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.
}