Linie wyrównania w Jetpack Compose

Model układu Compose umożliwia używanie elementu AlignmentLine do tworzenia niestandardowych linii wyrównania, których mogą używać układy nadrzędne do wyrównywania i pozycjonowania elementów podrzędnych. Na przykład element Row może używać niestandardowych linii wyrównania elementów podrzędnych do ich wyrównywania.

Gdy układ podaje wartość dla określonego elementu AlignmentLine, układy nadrzędne mogą odczytać tę wartość po pomiarze za pomocą operatora Placeable.get w odpowiednim wystąpieniu elementu Placeable. Na podstawie pozycji elementu AlignmentLine układy nadrzędne mogą następnie określić pozycjonowanie elementów podrzędnych.

Niektóre elementy kompozycyjne w Compose mają już linie wyrównania. Na przykład element kompozycyjny BasicText udostępnia FirstBaseline i LastBaseline linie wyrównania.

W poniższym przykładzie niestandardowy element LayoutModifier o nazwie firstBaselineToTop odczytuje element FirstBaseline, aby dodać do elementu Text dopełnienie zaczynające się od jego pierwszej linii bazowej.

Pokazuje różnicę między dodaniem zwykłego dopełnienia do elementu a zastosowaniem dopełnienia do linii bazowej elementu tekstowego.
Rysunek 1. Pokazuje różnicę między dodaniem zwykłego dopełnienia do elementu a zastosowaniem dopełnienia do linii bazowej elementu Text.

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

Aby odczytać element FirstBaseline w przykładzie, placeable [FirstBaseline] jest używany w fazie pomiaru.

Tworzenie niestandardowych linii wyrównania

Podczas tworzenia niestandardowego elementu kompozycyjnego Layout lub niestandardowego elementu LayoutModifier możesz podać niestandardowe linie wyrównania, aby inne nadrzędne elementy kompozycyjne mogły ich używać do wyrównywania i pozycjonowania elementów podrzędnych odpowiednio.

Poniższy przykład pokazuje niestandardowy element kompozycyjny BarChart, który udostępnia 2 linie wyrównania: MaxChartValue i MinChartValue. Dzięki temu inne elementy kompozycyjne mogą wyrównywać się do maksymalnej i minimalnej wartości danych na wykresie. Dwa elementy tekstowe, Max i Min, zostały wyrównane do środka niestandardowych linii wyrównania.

Kompozycja BarChart z tekstem wyrównanym do maksymalnej i minimalnej wartości danych.
Rysunek 2. Element kompozycyjny BarChart z tekstem wyrównanym do maksymalnej i minimalnej wartości danych.

Niestandardowe linie wyrównania są definiowane jako zmienne najwyższego poziomu w projekcie.

/**
 * 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)
})

Niestandardowe linie wyrównania, które tworzą nasz przykład, są typu HorizontalAlignmentLine, ponieważ służą do wyrównywania elementów podrzędnych w pionie. W przypadku, gdy wiele układów podaje wartość dla tych linii wyrównania, jako parametr przekazywana jest zasada scalania. Ponieważ system układu Compose i współrzędne elementu Canvas reprezentują wartość [0, 0], lewy górny róg, a osie x i y są dodatnie w dół, wartość MaxChartValue będzie zawsze mniejsza niż MinChartValue. Dlatego zasada scalania to min w przypadku linii bazowej maksymalnej wartości danych na wykresie i max w przypadku linii bazowej minimalnej wartości danych na wykresie.

Podczas tworzenia niestandardowego elementu Layout lub LayoutModifier określ niestandardowe linie wyrównania w metodzie MeasureScope.layout, która przyjmuje parametr alignmentLines: Map<AlignmentLine, Int>.

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

Linie wyrównania mogą być używane przez bezpośrednich i pośrednich rodziców tego elementu kompozycyjnego. Poniższy element kompozycyjny tworzy układ niestandardowy, który przyjmuje jako parametr 2 miejsca na element Text i punkty danych, a następnie wyrównuje 2 teksty do maksymalnej i minimalnej wartości danych na wykresie. Podgląd tego elementu kompozycyjnego jest widoczny na rysunku 2.

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