Linie wyrównania w Jetpack Compose

Model układu Compose umożliwia używanie AlignmentLine do tworzenia niestandardowych linii wyrównania, których układy nadrzędne mogą używać 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ównania.

Gdy układ udostępnia wartość dla konkretnego elementu AlignmentLine, jego elementy nadrzędne mogą odczytać tę wartość po pomiarze za pomocą operatora Placeable.get na odpowiedniej instancji Placeable. Na podstawie pozycji AlignmentLine rodzice mogą następnie określić pozycję dzieci.

Niektóre komponenty w Compose mają już linie wyrównania. Na przykład komponent BasicText udostępnia linie wyrównania FirstBaselineLastBaseline.

W poniższym przykładzie niestandardowy LayoutModifier o nazwie firstBaselineToTop odczytuje FirstBaseline, aby dodać do Text dopełnienie od pierwszej linii bazowej.

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

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ć FirstBaseline w przykładzie, w fazie pomiaru używa się placeable [FirstBaseline].

Tworzenie niestandardowych linii wyrównania

Podczas tworzenia niestandardowego elementu kompozycyjnego Layout lub niestandardowego elementu kompozycyjnego 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.

Layout

W tym przykładzie pokazano niestandardowy komponent BarChart, który udostępnia 2 linie wyrównania: MaxChartValueMinChartValue. Dzięki temu inne komponenty mogą się wyrównywać do maksymalnej i minimalnej wartości danych na wykresie. Dwa elementy tekstowe, MaxMin, zostały wyrównane do środka niestandardowych linii wyrównania.

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

Niestandardowe linie wyrównania są zdefiniowane 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 użyte w naszym przykładzie są typu HorizontalAlignmentLine, ponieważ służą do pionowego wyrównywania elementów podrzędnych. Zasada scalania jest przekazywana jako parametr, jeśli wiele układów zawiera wartość dla tych linii wyrównania. Ponieważ system układu Compose koordynuje, a współrzędne Canvas reprezentują [0, 0], lewy górny róg oraz osie xy są dodatnie w dół, wartość MaxChartValue będzie zawsze mniejsza niż MinChartValue. Dlatego zasady scalania to min w przypadku maksymalnej wartości danych wykresu i max w przypadku minimalnej wartości danych wykresu.

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

Bezpośredni i pośredni rodzice tego komponentu mogą korzystać z linii wyrównania. Poniższy komponent tworzy układ niestandardowy, który przyjmuje jako parametr 2 Text boksy i punkty danych oraz wyrównuje 2 teksty do maksymalnej i minimalnej wartości danych wykresu. Podgląd tego komponentu 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)
        )
    }
}