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 una AlignmentLine específica, los elementos superiores del diseño pueden leer este valor después de la medición mediante el operador Placeable.get en la instancia Placeable correspondiente. Según la posición de la AlignmentLine, los elementos superiores pueden decidir el posicionamiento de los elementos secundarios.

Algunos elementos de Compose que admiten composición ya incluyen líneas de alineación. Por ejemplo, el elemento BasicText, que admite composición, 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 del primer modelo de referencia.

Figura 1: Muestra la diferencia entre agregar padding normal a un elemento y aplicar padding al modelo de referencia 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
fun TextWithPaddingToBaselinePreview() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

Para leer el FirstBaseline del ejemplo, se usa placeable [FirstBaseline] en la fase de medición.

Cómo crear líneas de alineación personalizadas

Cuando se crea un elemento Layout personalizado que admite composición o un LayoutModifier personalizado, puedes proporcionar líneas de alineación personalizadas a fin de que otros elementos superiores que admiten composición puedan usarlas para alinear y posicionar los 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 en el centro de las líneas de alineación personalizadas.

Figura 2: BarChart que admite composición con el texto alineado al valor de datos máximo y mínimo.

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

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

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 de valor de datos del gráfico máximo y max para el modelo de referencia de valor de datos del gráfico mínimo.

Cuando crees un elemento Layout o un 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
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)
        }
    }
}

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