Misurazioni intrinseche nei layout di Scrivi

Una delle regole di Compose è che devi misurare i componenti secondari una sola volta; la misurazione dei componenti secondari due volte genera un'eccezione di runtime. Tuttavia, a volte hai bisogno di alcune informazioni sui tuoi nodi secondari prima di misurarli.

Le funzioni intrinseche ti consentono di eseguire query sugli elementi secondari prima che vengano effettivamente misurati.

A un composable, puoi chiedere IntrinsicSize.Min o IntrinsicSize.Max:

  • Modifier.width(IntrinsicSize.Min): qual è la larghezza minima necessaria per visualizzare correttamente i contenuti?
  • Modifier.width(IntrinsicSize.Max): qual è la larghezza massima necessaria per visualizzare correttamente i contenuti?
  • Modifier.height(IntrinsicSize.Min): qual è l'altezza minima necessaria per visualizzare correttamente i contenuti?
  • Modifier.height(IntrinsicSize.Max): qual è l'altezza massima necessaria per visualizzare correttamente i contenuti?

Ad esempio, se chiedi la minIntrinsicHeight di un Text con vincoli width infiniti in un layout personalizzato, viene restituita la height del Text con il testo disegnato su una singola riga.

Intrinsics in azione

Puoi creare un componibile che mostri due testi sullo schermo separati da un divisore:

Due elementi di testo affiancati, con un divisore verticale tra loro

Per farlo, utilizza un Row con due componibili Text che riempiono lo spazio disponibile e un Divider al centro. Divider deve essere alto quanto l'Text più alto e deve essere sottile (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
        )
    }
}

Divider si espande a schermo intero, il che non è il comportamento desiderato:

Due elementi di testo affiancati, con un divisore tra loro, ma il divisore si estende verso il basso oltre la parte inferiore del testo

Questo accade perché Row misura ogni elemento secondario singolarmente e l'altezza di Text non può essere utilizzata per vincolare Divider.

Per fare in modo che Divider riempia lo spazio disponibile con un'altezza specifica, utilizza invece il modificatore height(IntrinsicSize.Min).

height(IntrinsicSize.Min) dimensiona i suoi elementi secondari in modo che siano alti quanto la loro altezza intrinseca minima. Poiché questo modificatore è ricorsivo, esegue una query su minIntrinsicHeight di Row e dei relativi elementi secondari.

L'applicazione di questo modificatore al codice lo fa funzionare come previsto:

@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 anteprima:

Due elementi di testo affiancati, con un divisore verticale tra loro

L'altezza di Row viene determinata nel seguente modo:

  • La minIntrinsicHeight del composable Row è la minIntrinsicHeight massima dei suoi elementi secondari.
  • L'elemento minIntrinsicHeight di Divider è 0, in quanto non occupa spazio se non vengono forniti vincoli.
  • La Text minIntrinsicHeight è quella del testo per un width specifico.
  • Pertanto, il vincolo height dell'elemento Row diventa il valore massimo minIntrinsicHeight degli elementi Text.
  • Divider espande quindi il suo height al vincolo height fornito da Row.

Intrinsics nei layout personalizzati

Quando crei un modificatore Layout o layout personalizzato, le misurazioni intrinseche vengono calcolate automaticamente in base alle approssimazioni. Pertanto, i calcoli potrebbero non essere corretti per tutti i layout. Queste API offrono opzioni per eseguire l'override di queste impostazioni predefinite.

Per specificare le misurazioni intrinseche del tuo Layout personalizzato, esegui l'override di minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth e maxIntrinsicHeight dell'interfaccia MeasurePolicy durante la creazione. L'override deve corrispondere a ciò che fa measure. Ad esempio, in una colonna personalizzata che impila i relativi elementi secondari verticalmente e assegna a ciascuno di essi l'intera larghezza della colonna, l'altezza intrinseca minima per un determinato width è la somma delle altezze intrinseche minime degli elementi secondari per quel 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.
        }
    )
}

Quando crei il modificatore layout personalizzato, esegui l'override dei metodi correlati nell'interfaccia LayoutModifier. Il calcolo intrinseco deve corrispondere a measure. Ad esempio, per un determinato height, un modificatore che aggiunge un padding fisso su tutti i lati sottrae il padding verticale prima di eseguire una query su minIntrinsicWidth dell'elemento secondario e aggiunge il padding orizzontale al risultato:

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