Líneas de alineación de Jetpack Compose

El modelo de diseño de Compose te permite usar AlignmentLine para crear líneas de alineación personalizadas que los diseños de nivel superior pueden usar para alinear y posicionar sus elementos secundarios. Por ejemplo, Row puede usar las líneas de alineación personalizadas de sus elementos secundarios para alinearlos.

Cuando un diseño proporciona un valor para un AlignmentLine en particular, los elementos superiores del diseño pueden leer este valor después de la medición, con el operador Placeable.get en la instancia Placeable correspondiente. Según la posición del AlignmentLine, los padres pueden decidir la posición de los elementos secundarios.

Algunos elementos de Compose que admiten composición ya incluyen líneas de alineación. Por ejemplo, el elemento componible BasicText expone las líneas de alineación FirstBaseline y LastBaseline.

En el siguiente ejemplo, un LayoutModifier personalizado llamado firstBaselineToTop lee el FirstBaseline para agregar padding al Text a partir de su primera línea de base.

Muestra la diferencia entre agregar padding normal a un elemento y aplicar padding al modelo de referencia de un elemento de texto.
Figura 1: Muestra la diferencia entre agregar padding normal a un elemento y aplicar padding a la línea de base de un 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 leer el FirstBaseline en el ejemplo, se usa placeable [FirstBaseline] en la fase de medición.

Crea líneas de alineación personalizadas

Cuando creas un elemento de componibilidad Layout o un LayoutModifier personalizados, puedes proporcionar líneas de alineación personalizadas para que otros elementos de componibilidad superiores puedan usarlas para alinear y posicionar sus elementos secundarios según corresponda.

En el siguiente ejemplo, se muestra un elemento BarChart que admite composición y muestra dos líneas de alineación, MaxChartValue y MinChartValue, de modo que otros elementos que admitan composición puedan alinearse con el valor de datos máximo y mínimo del gráfico. Dos elementos de texto, Max y Min, se alinearon con el centro de las líneas de alineación personalizadas.

Elemento componible BarChart con texto alineado con los valores de datos máximo y mínimo
Figura 2: BarChart componible con texto alineado con el valor de datos máximo y mínimo

Las líneas de alineación personalizadas se definen como variables de nivel superior en tu proyecto.

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

Las líneas de alineación personalizadas que se utilizan en la creación de nuestro ejemplo son del tipo HorizontalAlignmentLine, ya que su función es alinear verticalmente los elementos secundarios. Se pasa una política de combinación como parámetro en caso de que varios diseños proporcionen un valor para estas líneas de alineación. Mientras las coordenadas del sistema de diseño de Compose y las coordenadas de Canvas representen [0, 0], la esquina superior izquierda y los ejes x y y serán positivos hacia abajo, para que el valor MaxChartValue siempre sea menor que MinChartValue. Por lo tanto, la política de combinación es min para el modelo de referencia del valor máximo de los datos del gráfico y max para el modelo de referencia del valor mínimo de los datos del gráfico.

Cuando crees un Layout o LayoutModifier personalizado, especifica líneas de alineación personalizadas en el método MeasureScope.layout, que toma un 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()
                        )
                    ) {}
                }
            }
        }
    }
}

Los elementos superiores directos e indirectos de este elemento que admite composición pueden consumir las líneas de alineación. El siguiente elemento que admite composición crea un diseño personalizado que toma como parámetro dos ranuras y datos Text, y alinea los dos textos con los valores de datos de los gráficos máximo y mínimo. La vista previa de este elemento que admite composición es la que se muestra en la 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)
        )
    }
}