Ausrichtungslinien in Jetpack Compose

Mit dem Compose-Layoutmodell können Sie AlignmentLine verwenden, um benutzerdefinierte Ausrichtungslinien zu erstellen, die von übergeordneten Layouts verwendet werden können, um ihre untergeordneten Elemente auszurichten und zu positionieren. Beispielsweise kann Row die benutzerdefinierten Ausrichtungslinien seiner untergeordneten Elemente verwenden, um sie auszurichten.

Wenn ein Layout einen Wert für ein bestimmtes AlignmentLine bereitstellt, können die übergeordneten Elemente des Layouts diesen Wert nach der Messung mit dem Operator Placeable.get für die entsprechende Placeable-Instanz lesen. Anhand der Position des AlignmentLine können die Eltern dann die Positionierung der Kinder festlegen.

Einige Composeables in Compose haben bereits Ausrichtungslinien. Beispielsweise stellt die komponierbare Funktion BasicText die Ausrichtungslinien FirstBaseline und LastBaseline bereit.

Im folgenden Beispiel liest ein benutzerdefiniertes LayoutModifier namens firstBaselineToTop den FirstBaseline, um der Text ab der ersten Referenzlinie einen Abstand hinzuzufügen.

Zeigt den Unterschied zwischen dem Hinzufügen eines normalen Abstands zu einem Element und dem Anwenden eines Abstands auf die Grundlinie eines Textelements.
Abbildung 1: Zeigt den Unterschied zwischen dem Hinzufügen eines normalen Abstands zu einem Element und dem Anwenden eines Abstands auf die Grundlinie eines Textelements.

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp,
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

@Preview
@Composable
private fun TextWithPaddingToBaseline() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

Um die FirstBaseline im Beispiel zu lesen, wird in der Messphase placeable [FirstBaseline] verwendet.

Benutzerdefinierte Ausrichtungslinien erstellen

Wenn Sie ein benutzerdefiniertes Layout- oder LayoutModifier-Composable erstellen, können Sie benutzerdefinierte Ausrichtungslinien angeben, damit andere übergeordnete Composables sie verwenden können, um ihre untergeordneten Elemente entsprechend auszurichten und zu positionieren.

Im folgenden Beispiel wird eine benutzerdefinierte BarChart-komponierbare Funktion gezeigt, die zwei Ausrichtungslinien (MaxChartValue und MinChartValue) verfügbar macht, damit andere komponierbare Funktionen am maximalen und minimalen Datenwert des Diagramms ausgerichtet werden können. Zwei Textelemente, Max und Min, wurden an der Mitte der benutzerdefinierten Ausrichtungslinien ausgerichtet.

BarChart-Komposition mit Text, der am maximalen und minimalen Datenwert ausgerichtet ist.
Abbildung 2. BarChart-Composable mit Text, der am maximalen und minimalen Datenwert ausgerichtet ist.

Benutzerdefinierte Ausrichtungslinien werden als Variablen auf oberster Ebene in Ihrem Projekt definiert.

/**
 * AlignmentLine defined by the maximum data value in a [BarChart]
 */
private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new ->
    min(old, new)
})

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
private val MinChartValue = HorizontalAlignmentLine(merger = { old, new ->
    max(old, new)
})

Die benutzerdefinierten Ausrichtungslinien zum Erstellen unseres Beispiels sind vom Typ HorizontalAlignmentLine, da sie zum vertikalen Ausrichten von untergeordneten Elementen verwendet werden. Eine Zusammenführungsrichtlinie wird als Parameter übergeben, falls mehrere Layouts einen Wert für diese Ausrichtungslinien bereitstellen. Da die Koordinaten des Compose-Layoutsystems und die Canvas-Koordinaten [0, 0] darstellen, sind die obere linke Ecke und die x- und y-Achse nach unten positiv, sodass der MaxChartValue-Wert immer kleiner als MinChartValue ist. Daher ist die Zusammenführungsrichtlinie für die Referenzlinie des maximalen Diagrammdatenwerts min und für die Referenzlinie des minimalen Diagrammdatenwerts max.

Wenn Sie eine benutzerdefinierte Layout oder LayoutModifier erstellen, geben Sie benutzerdefinierte Ausrichtungslinien in der Methode MeasureScope.layout an, die einen alignmentLines: Map<AlignmentLine, Int>-Parameter akzeptiert.

@Composable
private fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier,
) {
    val maxValue: Float = remember(dataPoints) { dataPoints.maxOrNull()!! * 1.2f }

    BoxWithConstraints(modifier = modifier) {
        val density = LocalDensity.current
        with(density) {
            // ...
            // Calculate baselines
            val maxYBaseline = // ...
            val minYBaseline = // ...
            Layout(
                content = {},
                modifier = Modifier.drawBehind {
                    // ...
                }
            ) { _, constraints ->
                with(constraints) {
                    layout(
                        width = if (hasBoundedWidth) maxWidth else minWidth,
                        height = if (hasBoundedHeight) maxHeight else minHeight,
                        // Custom AlignmentLines are set here. These are propagated
                        // to direct and indirect parent composables.
                        alignmentLines = mapOf(
                            MinChartValue to minYBaseline.roundToInt(),
                            MaxChartValue to maxYBaseline.roundToInt()
                        )
                    ) {}
                }
            }
        }
    }
}

Direkte und indirekte übergeordnete Elemente dieser zusammensetzbaren Funktion können die Ausrichtungslinien nutzen. Die folgende zusammensetzbare Funktion erstellt ein benutzerdefiniertes Layout, das zwei Text-Slots und Datenpunkte als Parameter verwendet und die beiden Texte an den maximalen und minimalen Diagrammdatenwerten ausrichtet. Die Vorschau dieser zusammensetzbaren Funktion ist in Abbildung 2 zu sehen.

@Composable
private fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Layout(
        content = {
            maxText()
            minText()
            // Set a fixed size to make the example easier to follow
            BarChart(dataPoints, Modifier.size(200.dp))
        },
        modifier = modifier
    ) { measurables, constraints ->
        check(measurables.size == 3)
        val placeables = measurables.map {
            it.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        val maxTextPlaceable = placeables[0]
        val minTextPlaceable = placeables[1]
        val barChartPlaceable = placeables[2]

        // Obtain the alignment lines from BarChart to position the Text
        val minValueBaseline = barChartPlaceable[MinChartValue]
        val maxValueBaseline = barChartPlaceable[MaxChartValue]
        layout(constraints.maxWidth, constraints.maxHeight) {
            maxTextPlaceable.placeRelative(
                x = 0,
                y = maxValueBaseline - (maxTextPlaceable.height / 2)
            )
            minTextPlaceable.placeRelative(
                x = 0,
                y = minValueBaseline - (minTextPlaceable.height / 2)
            )
            barChartPlaceable.placeRelative(
                x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
                y = 0
            )
        }
    }
}
@Preview
@Composable
private fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}