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 IntrinsicSize.Min o IntrinsicSize.Max:

  • Modifier.width(IntrinsicSize.Min): ¿Cuál es el ancho mínimo que necesitas para mostrar tu contenido correctamente?
  • Modifier.width(IntrinsicSize.Max): ¿Cuál es el ancho máximo que necesitas para mostrar tu contenido correctamente?
  • Modifier.height(IntrinsicSize.Min): ¿Cuál es la altura mínima que necesitas para mostrar tu contenido correctamente?
  • Modifier.height(IntrinsicSize.Max): ¿Cuál es la altura máxima que necesitas para mostrar tu contenido correctamente?

Por ejemplo, si solicitas la minIntrinsicHeight de un Text con restricciones de width infinitas en un diseño personalizado, se mostrará la height del Text con el texto dibujado en una sola línea.

Funciones intrínsecas en acción

Puedes crear un elemento componible que muestre dos textos en la pantalla separados por un divisor:

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

Para ello, usa un Row con dos elementos Text componibles que llenen el espacio disponible y un Divider en el medio. El Divider debe ser 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
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

El elemento Divider se expande a toda la pantalla, lo que no es el comportamiento deseado:

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.

Para que el Divider ocupe el espacio disponible con una altura determinada, usa el modificador height(IntrinsicSize.Min).

height(IntrinsicSize.Min) ajusta el tamaño de sus elementos secundarios para que sean tan altos como su altura mínima intrínseca. Dado que este modificador es recursivo, consulta el minIntrinsicHeight del Row y sus elementos secundarios.

Si aplicas este modificador a tu 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
        )
        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")
        }
    }
}

Con vista previa:

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

La altura de Row se determina de la siguiente manera:

  • El minIntrinsicHeight del elemento componible Row es el minIntrinsicHeight máximo de sus elementos secundarios.
  • El minIntrinsicHeight del elemento Divider es 0, ya que no ocupa espacio si no se le aplican restricciones.
  • El Text minIntrinsicHeight es el del texto para un width específico.
  • Por lo tanto, la restricción height del elemento Row se convierte en la minIntrinsicHeight máxima de los Text.
  • Luego, Divider expande su height a la restricción de 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 la interfaz MeasurePolicy cuando la crees.

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