Ausrichtungslinien in Jetpack Compose

Im Layoutmodell „Compose“ können Sie mit AlignmentLine benutzerdefinierte Ausrichtungslinien erstellen, die von übergeordneten Layouts zum Ausrichten und Positionieren der untergeordneten Elemente verwendet werden können. Beispielsweise kann Row die benutzerdefinierten Ausrichtungslinien seiner untergeordneten Elemente verwenden, um sie auszurichten.

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

Einige Compose-Elemente haben bereits Ausrichtungslinien. Mit dem BasicText-Komposit werden beispielsweise die Ausrichtungslinien FirstBaseline und LastBaseline eingeblendet.

Im folgenden Beispiel liest ein benutzerdefinierter LayoutModifier namens firstBaselineToTop den FirstBaseline, um der Text ab ihrer ersten Grundlinie ein Abstand hinzuzufügen.

Abbildung 1: Zeigt den Unterschied zwischen dem Hinzufügen eines normalen Innenrands für ein Element und dem Anwenden eines Innenrands auf die Referenz 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 das FirstBaseline im Beispiel zu lesen, wird in der Messphase placeable [FirstBaseline] verwendet.

Benutzerdefinierte Fluchtlinien erstellen

Wenn Sie ein benutzerdefiniertes Layout- oder LayoutModifier-Element erstellen, können Sie benutzerdefinierte Ausrichtungslinien angeben, mit denen andere übergeordnete Elemente ihre untergeordneten Elemente entsprechend ausrichten und positionieren können.

Das folgende Beispiel zeigt eine benutzerdefinierte zusammensetzbare Funktion BarChart, die zwei Ausrichtungslinien, MaxChartValue und MinChartValue, enthält, damit andere zusammensetzbare Funktionen 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 mit Text, der am Höchst- und Mindestdatenwert ausgerichtet ist.

Benutzerdefinierte Ausrichtungslinien werden in Ihrem Projekt als Variablen der obersten Ebene 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 haben den 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 angeben. Da die Koordinaten des Layoutsystems und die Canvas-Koordinaten [0, 0] darstellen, sind die obere linke Ecke sowie die Achse x und y nach unten positiv, sodass der Wert von MaxChartValue immer kleiner als MinChartValue ist. Daher ist die Zusammenführungsrichtlinie min für den Höchstwert der Diagrammdaten und max für den Mindestwert der Diagrammdaten.

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

@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 dieses Composeables können die Ausrichtungslinien verwenden. Mit dem folgenden Composeable wird ein benutzerdefiniertes Layout erstellt, das als Parameter zwei Text-Slots und Datenpunkte annimmt und die beiden Texte an den Maximal- und Minimalwerten der Diagrammdaten ausrichtet. Abbildung 2 zeigt die Vorschau dieses Composeables.

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