Linie wyrównania w Jetpack Compose

Model układu w Compose umożliwia użycie 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 swoich podrzędnych elementów, aby je wyrównać.

Gdy układ zawiera wartość dla określonego parametru AlignmentLine, jego rodzice mogą odczytać tę wartość po zmierzeniu, używając operatora Placeable.get w odpowiednim wystąpieniu Placeable. Na podstawie pozycji AlignmentLine rodzice mogą określić pozycję dzieci.

Niektóre komponenty w Compose mają już linie wyrównywania. Na przykład parametr BasicText funkcja kompozycyjna ujawnia wiersze wyrównania FirstBaseline i LastBaseline.

W przykładzie poniżej niestandardowa funkcja LayoutModifier o nazwie firstBaselineToTop odczytuje wartość FirstBaseline, aby dodać wypełnienie do elementu Text, zaczynając od jego pierwszej wartości odniesienia.

Rysunek 1. pokazuje różnicę między dodaniem zwykłego dopełnienia do elementu; i dopełnienie do podstawy 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 z przykładu, W fazie pomiaru jest używany parametr placeable [FirstBaseline].

Tworzenie niestandardowych linii wyrównania

Podczas tworzenia niestandardowego komponentu Layout lub niestandardowego komponentu LayoutModifier możesz dodać niestandardowe linie wyrównania, aby inne nadrzędne komponenty mogły ich używać do wyrównywania i odpowiedniego pozycjonowania swoich elementów podrzędnych.

W tym przykładzie pokazano niestandardowy komponent BarChart, który udostępnia 2 linie wyrównania, MaxChartValueMinChartValue, aby inne komponenty mogły się dopasować do maksymalnej i minimalnej wartości danych na wykresie. Dwa teksty elementy Max i Min zostały wyrównane do środka wymiaru niestandardowego linii wyrównania.

Rysunek 2. Funkcja BarChart kompozycyjna 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 użyte w naszym przykładzie są typu HorizontalAlignmentLine, ponieważ służą do wyrównywania elementów pionowo. Jeśli wiele układów zawiera wartość dla tych linii wyrównania, jako parametr przekazywana jest zasada łączenia. Jako współrzędne systemowe układu tworzenia wiadomości oraz Canvas współrzędne reprezentują [0, 0], lewy górny róg, a oś x i y są jest dodatni, więc wartość MaxChartValue będzie zawsze mniejsza od MinChartValue. Dlatego zasada scalania w przypadku maksymalnego wykresu to min. jako punkt odniesienia dla minimalnej wartości danych na wykresie, a jako punkt odniesienia dla minimalnej wartości danych na wykresie – max.

Podczas tworzenia niestandardowego wyrównania Layout lub LayoutModifier określ wyrównanie niestandardowe wiersze w MeasureScope.layout , która wymaga 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średnie i pośrednie elementy nadrzędne tej funkcji kompozycyjnej mogą wykorzystać dopasowanie . Poniższa kompozycja tworzy układ niestandardowy, który przyjmuje jako parametry 2 boksy Text i punkty danych, a następnie wyrównuje 2 teksty do wartości maksymalnych i minimalnych danych wykresu. Podgląd tego elementu kompozycyjnego to co widać 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)
        )
    }
}