Systeminterne Messungen in Compose-Layouts

Eine der Regeln von Compose ist, dass Sie Ihre untergeordneten Elemente nur einmal messen sollten. Wenn Sie untergeordnete Elemente zweimal messen, wird eine Laufzeitausnahme ausgelöst. Manchmal benötigen Sie jedoch Informationen zu Ihren untergeordneten Elementen, bevor Sie sie messen.

Mit intrinsischen Größen können Sie untergeordnete Elemente abfragen, bevor sie tatsächlich gemessen werden.

Sie können eine zusammensetzbare Funktion nach ihrem IntrinsicSize.Min oder IntrinsicSize.Max fragen:

  • Modifier.width(IntrinsicSize.Min): Wie breit muss der Bildschirm mindestens sein, damit Ihre Inhalte richtig dargestellt werden?
  • Modifier.width(IntrinsicSize.Max): Wie breit muss der Bildschirm maximal sein, damit Ihre Inhalte richtig angezeigt werden?
  • Modifier.height(IntrinsicSize.Min): Wie hoch muss die Anzeige mindestens sein, damit Ihre Inhalte richtig dargestellt werden?
  • Modifier.height(IntrinsicSize.Max): Wie hoch muss die Anzeige maximal sein, damit Ihre Inhalte richtig dargestellt werden?

Wenn Sie beispielsweise die minIntrinsicHeight eines Text mit unendlichen width-Einschränkungen in einem benutzerdefinierten Layout abfragen, wird die height des Text mit dem Text in einer einzigen Zeile zurückgegeben.

Intrinsische Werte in Aktion

Du kannst eine zusammensetzbare Funktion erstellen, die zwei Texte auf dem Bildschirm anzeigt, die durch eine Trennlinie getrennt sind:

Zwei Textelemente nebeneinander, mit einer vertikalen Trennlinie dazwischen

Verwenden Sie dazu eine Row mit zwei Text-Kompositionen, die den verfügbaren Platz ausfüllen, und einer Divider in der Mitte. Der Divider sollte so hoch wie der höchste Text und dünn (width = 1.dp) sein.

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

Die Divider wird auf den gesamten Bildschirm maximiert, was nicht dem gewünschten Verhalten entspricht:

Zwei Textelemente nebeneinander, mit einer Trennlinie dazwischen, die sich jedoch unter den unteren Rand des Textes erstreckt

Das liegt daran, dass Row jedes untergeordnete Element einzeln misst und die Höhe von Text nicht verwendet werden kann, um die Divider einzuschränken.

Wenn die Divider stattdessen den verfügbaren Platz mit einer bestimmten Höhe füllen soll, verwenden Sie den Modifikator height(IntrinsicSize.Min).

height(IntrinsicSize.Min) passt die Größe seiner untergeordneten Elemente so an, dass sie so hoch sind wie ihre minimale intrinsische Höhe. Da dieser Modifikator rekursiv ist, werden die minIntrinsicHeight der Row und ihrer untergeordneten Elemente abgefragt.

Wenn Sie diesen Modifikator auf Ihren Code anwenden, funktioniert er wie erwartet:

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

Mit Vorschau:

Zwei Textelemente nebeneinander, mit einer vertikalen Trennlinie dazwischen

Die Höhe des Row wird so bestimmt:

  • Der minIntrinsicHeight des Row-Kompositums ist der maximale minIntrinsicHeight seiner untergeordneten Elemente.
  • Der Divider-Wert des minIntrinsicHeight-Elements ist 0, da es keinen Platz einnimmt, wenn keine Einschränkungen angegeben sind.
  • Der Text-minIntrinsicHeight ist der des Texts für eine bestimmte width.
  • Daher wird die Einschränkung height des Elements Row zum maximalen minIntrinsicHeight der Texts.
  • Der Divider erweitert dann seine height auf die height-Einschränkung, die vom Row vorgegeben wird.

Intrinsische Werte in benutzerdefinierten Layouts

Wenn Sie einen benutzerdefinierten Layout- oder layout-Modifikator erstellen, werden intrinsische Messungen automatisch anhand von Näherungswerten berechnet. Daher sind die Berechnungen möglicherweise nicht für alle Layouts korrekt. Diese APIs bieten Optionen zum Überschreiben dieser Standardeinstellungen.

Wenn Sie die intrinsischen Messungen Ihres benutzerdefinierten Layout angeben möchten, überschreiben Sie beim Erstellen die minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth und maxIntrinsicHeight der MeasurePolicy-Schnittstelle. Ihre Überschreibung muss mit der Funktion von measure übereinstimmen. In einer benutzerdefinierten Spalte, in der die untergeordneten Elemente vertikal gestapelt werden und jedes untergeordnete Element die volle Spaltenbreite erhält, ist die minimale intrinsische Höhe für eine bestimmte width beispielsweise die Summe der minimalen intrinsischen Höhen der untergeordneten Elemente für diese 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.
        }
    )
}

Wenn Sie einen benutzerdefinierten layout-Modifikator erstellen, überschreiben Sie die zugehörigen Methoden in der Schnittstelle LayoutModifier. Die intrinsische Berechnung muss mit measure übereinstimmen. Beispiel: Bei einem bestimmten height subtrahiert ein Modifikator, der auf allen Seiten einen festen Abstand hinzufügt, den vertikalen Abstand, bevor er die minIntrinsicWidth des untergeordneten Elements abfragt, und fügt dem Ergebnis den horizontalen Abstand hinzu:

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