Ausrichtungslinien in Jetpack Compose

Mit dem Compose-Layoutmodell können Sie mit AlignmentLine benutzerdefinierte Ausrichtungslinien erstellen, die von übergeordneten Layouts verwendet werden können, um ihre untergeordneten Elemente auszurichten und zu positionieren. Row kann beispielsweise 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 untergeordneten Elemente festlegen.

Einige Composables in Compose haben bereits Ausrichtungslinien. Die BasicText-Composable-Funktion stellt beispielsweise die Ausrichtungslinien FirstBaseline und LastBaseline bereit.

Im Beispiel unten wird ein benutzerdefiniertes LayoutModifier mit dem Namen firstBaselineToTop verwendet, um die FirstBaseline zu lesen und der Text ab der ersten Baseline Padding hinzuzufügen.

Abbildung 1: Hier sehen Sie den Unterschied zwischen dem Hinzufügen von normalem Padding zu einem Element und dem Anwenden von Padding 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 placeable [FirstBaseline] in der Messphase verwendet.

Benutzerdefinierte Ausrichtungslinien erstellen

Wenn Sie eine benutzerdefinierte Layout-Composable oder eine benutzerdefinierte LayoutModifier 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 ein benutzerdefiniertes BarChart-Composable gezeigt, das zwei Ausrichtungslinien, MaxChartValue und MinChartValue, bereitstellt, damit andere Composables am maximalen und minimalen Datenwert des Diagramms ausgerichtet werden können. Die beiden Textelemente Max und Min wurden an der Mitte der benutzerdefinierten Ausrichtungslinien ausgerichtet.

Abbildung 2: BarChart, die mit Text ausgerichtet ist, der dem maximalen und minimalen Datenwert entspricht.

Benutzerdefinierte Ausrichtungslinien werden als Variablen der obersten 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 für unser Beispiel 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, die obere linke Ecke und die x- und y-Achse positiv nach unten verlaufen, ist der MaxChartValue-Wert immer kleiner als MinChartValue. Daher ist die Zusammenführungsrichtlinie min für die Baseline des maximalen Diagrammdatenwerts und max für die Baseline des minimalen Diagrammdatenwerts.

Wenn Sie ein benutzerdefiniertes 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 Eltern dieses Composables können die Ausrichtungslinien verwenden. Mit dem folgenden Composable wird ein benutzerdefiniertes Layout erstellt, das zwei Text-Slots und Datenpunkte als Parameter verwendet und die beiden Texte an den maximalen und minimalen Datenwerten des Diagramms ausrichtet. Die Vorschau dieser Komponente 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)
        )
    }
}