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 que pode ser composto 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 que pode ser composto ou um LayoutModifier personalizado, você pode fornecer linhas de alinhamento personalizadas para que outros elementos pai que podem ser compostos possam usá-las para alinhar e posicionar os filhos adequadamente.

O exemplo a seguir mostra um BarChart personalizado que pode ser composto que expõe duas linhas de alinhamento, MaxChartValue e MinChartValue, para que outros elementos que podem ser compostos se alinhem aos valores máximo e mínimo dos dados do gráfico. Dois elementos de texto, Max e Min, foram alinhados ao 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
) {
    var maxValueBaseline by remember { mutableStateOf(Float.MAX_VALUE) }
    var minValueBaseline by remember { mutableStateOf(Float.MIN_VALUE) }

    Layout(
        modifier = modifier,
        content = {
            // ... Logic to draw the chart in a Canvas ...
            // maxValueBaseline and minValueBaseline are updated here
        }
    ) { measurables, constraints ->
        val placeable = measurables[0].measure(constraints)
        layout(
            width = constraints.maxWidth,
            height = constraints.maxHeight,
            // Custom AlignmentLines are set here. These are propagated
            // to direct and indirect parent composables.
            alignmentLines = mapOf(
                MinChartValue to minValueBaseline.roundToInt(),
                MaxChartValue to maxValueBaseline.roundToInt()
            )
        ) {
            placeable.placeRelative(0, 0)
        }
    }
}

Os pais diretos e indiretos desse elemento que pode ser composto podem consumir as linhas de alinhamento. O seguinte elemento que pode ser composto 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 que pode ser composto é 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)
        )
    }
}