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

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

Criar linhas de alinhamento personalizadas

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

O exemplo a seguir mostra um BarChartcombináveis personalizado que expõe duas linhas de alinhamento, MaxChartValue e MinChartValue, para que outros elementos combináveis 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.

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

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

Os pais diretos e indiretos desse elemento combinável podem consumir as linhas de alinhamento. O seguinte elemento combinável 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
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)
        )
    }
}