Линии выравнивания в Jetpack Compose

Модель макета Compose позволяет использовать AlignmentLine для создания специальных линий выравнивания, которые могут использоваться родительскими макетами для выравнивания и позиционирования дочерних элементов. Например, Row может использовать специальные линии выравнивания дочерних элементов, чтобы выровнять их.

Если макет задает значение для определенного AlignmentLine, родительские элементы макета могут прочитать это значение после измерения, используя оператор Placeable.get для соответствующего экземпляра Placeable. Исходя из положения AlignmentLine, родители могут решить, как расположить детей.

Некоторые компоненты Compose уже имеют линии выравнивания. Например, composable-функция BasicText предоставляет линии выравнивания FirstBaseline и LastBaseline.

В следующем примере пользовательский тег LayoutModifier с названием firstBaselineToTop считывает тег FirstBaseline, чтобы добавить отступ к тегу Text, начиная с его первой базовой линии.

Показывает разницу между добавлением обычных полей к элементу и применением полей к базовой линии текстового элемента.
Рисунок 1. Показывает разницу между добавлением обычных полей к элементу и применением полей к базовой линии текстового элемента.

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

Чтобы прочитать FirstBaseline в примере, на этапе измерения используется placeable [FirstBaseline].

Как создать собственные линии выравнивания

При создании собственной composable-функции Layout или собственного элемента LayoutModifier можно задать собственные линии выравнивания, чтобы другие родительские composable-функции могли использовать их для выравнивания и позиционирования своих дочерних элементов.

В примере ниже показан пользовательский компонент BarChart, который предоставляет две линии выравнивания: MaxChartValue и MinChartValue. Благодаря этому другие компоненты могут выравниваться по максимальному и минимальному значению данных на диаграмме. Два текстовых элемента, Max и Min, выровнены по центру пользовательских линий выравнивания.

Компонуемый элемент BarChart с текстом, выровненным по максимальному и минимальному значению данных.
Рисунок 2. BarChartcomposable-функция с текстом, выровненным по максимальному и минимальному значению данных.

Пользовательские линии выравнивания определяются как переменные верхнего уровня в проекте.

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

В нашем примере используются пользовательские линии выравнивания типа HorizontalAlignmentLine, поскольку они нужны для вертикального выравнивания дочерних элементов. Правило объединения передается в качестве параметра, если несколько макетов предоставляют значение для этих линий выравнивания. Поскольку система макетов Compose использует координаты, а координаты Canvas представляют [0, 0], верхний левый угол и оси x и y положительны вниз, значение MaxChartValue всегда будет меньше, чем MinChartValue. Таким образом, для базового значения максимального значения данных диаграммы используется правило объединения min, а для базового значения минимального значения данных диаграммы – max.

При создании специального элемента Layout или LayoutModifier укажите специальные линии выравнивания в методе MeasureScope.layout, который принимает параметр 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()
                        )
                    ) {}
                }
            }
        }
    }
}

Прямые и косвенные родительские элементы этой composable-функции могут использовать линии выравнивания. Следующая composable-функция создает специальный макет, который принимает в качестве параметра два слота Text и точки данных и выравнивает два текста по максимальному и минимальному значениям данных диаграммы. Предварительный просмотр этой composable-функции показан на рисунке 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)
        )
    }
}