Linhas de alinhamento no Jetpack Compose

O modelo de layout do Compose possibilita o uso da AlignmentLine para criar linhas de alinhamento personalizadas que podem ser usadas por layouts pais para alinhar e posicionar os filhos. Por exemplo, a Row pode usar as linhas de alinhamento personalizadas dos filhos para alinhá-los.

Quando um layout fornece um valor para uma determinada AlignmentLine, os pais do layout podem ler esse valor após a medição, usando o operador Placeable.get na instância do Placeable correspondente. Com base na posição da AlignmentLine, os pais podem decidir o posicionamento dos filhos.

Alguns elementos que podem ser compostos no Compose já vêm com linhas de alinhamento. Por exemplo, o BasicText de composição expõe as linhas de alinhamento FirstBaseline e LastBaseline.

No exemplo abaixo, um LayoutModifier personalizado chamado firstBaselineToTop lê a FirstBaseline para adicionar padding ao Text começando pela primeira linha de base.

Figura 1. Mostra a diferença entre adicionar padding normal a um elemento e aplicar padding à linha de base de um elemento de texto.

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
fun TextWithPaddingToBaselinePreview() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

Para ler a FirstBaseline no exemplo, placeable [FirstBaseline] é usado na fase de medição.

Como criar linhas de alinhamento personalizadas

Ao criar um Layout de composição ou um LayoutModifier personalizado, você pode fornecer linhas de alinhamento personalizadas para que outros elementos de composição pai possam usá-las para alinhar e posicionar os filhos adequadamente.

O exemplo a seguir mostra um BarChartde composição personalizado que expõe duas linhas de alinhamento, MaxChartValue e MinChartValue, para que outros elementos de composição se alinhem aos valores máximo e mínimo dos dados do gráfico. Dois elementos de texto, Max e Min, foram alinhados com o centro das linhas de alinhamento personalizadas.

Figura 2. BarChart que pode ser composto com texto alinhado ao valor máximo e mínimo dos dados.

As linhas de alinhamento personalizadas são definidas como variáveis de nível superior no seu projeto.

import kotlin.math.max
import kotlin.math.min
import androidx.compose.ui.layout.HorizontalAlignmentLine

/**
 * AlignmentLine defined by the maximum data value in a [BarChart]
 */
val MaxChartValue = HorizontalAlignmentLine(merger = { old, new -> min(old, new) })

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
val MinChartValue = HorizontalAlignmentLine(merger = { old, new -> max(old, new) })

As linhas de alinhamento personalizadas para criar nosso exemplo são do tipo HorizontalAlignmentLine, porque são usadas para alinhar filhos verticalmente. Uma política de combinação é transmitida como um parâmetro quando vários layouts fornecem um valor para essas linhas de alinhamento. Como as coordenadas do sistema de layout do Compose e as coordenadas Canvas representam [0, 0], o canto superior esquerdo e os eixos x e y são positivos para baixo. Portanto, o valor MaxChartValue sempre será menor que o MinChartValue. Portanto, a política de combinação é min para o valor de referência máximo dos dados do gráfico e max para o valor de referência mínimo dos dados do gráfico.

Ao criar um Layout ou LayoutModifier personalizado, especifique linhas de alinhamento personalizadas no método MeasureScope.layout, que usa um parâmetro alignmentLines: Map<AlignmentLine, Int>.

@Composable
fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier
) {
    /* ... */
    BoxWithConstraints(modifier = modifier) {
        // Calculate custom AlignmentLines: minYBaseline and maxYBaseline
        val maxYBaseline = /* ... */
        val minYBaseline = /* ... */

        Layout(
            content = {},
            modifier = Modifier.drawBehind {
                // Logic to draw the Chart
            }
        ) { _, 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()
                    )
                ) {}
            }
        }
    }
}

Os pais diretos e indiretos desse elemento que pode ser composto podem consumir as linhas de alinhamento. O seguinte elemento de composição cria um layout personalizado que usa como parâmetro dois slots Text e pontos de dados, e alinha os dois textos com os valores máximos e mínimos do gráfico. A visualização desse elemento é mostrada na Figura 2.

@Composable
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
fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}