Linee di allineamento in Jetpack Compose

Il modello di layout di Compose ti consente di utilizzare AlignmentLine per creare linee di allineamento personalizzate che possono essere utilizzate dai layout principali per allineare e posizionare i relativi elementi secondari. Ad esempio, Row può utilizzare le linee di allineamento personalizzate dei relativi elementi secondari per allinearli.

Quando un layout fornisce un valore per un determinato AlignmentLine, i genitori del layout possono leggere questo valore dopo la misurazione, utilizzando l'operatore Placeable.get sull'istanza Placeable corrispondente. In base alla posizione di AlignmentLine, i genitori possono quindi decidere il posizionamento degli elementi secondari.

Alcuni composable in Compose sono già dotati di linee di allineamento. Ad esempio, il BasicText composable espone le FirstBaseline e LastBaseline linee di allineamento.

Nell'esempio seguente, un LayoutModifier personalizzato chiamato firstBaselineToTop legge FirstBaseline per aggiungere un padding al Text a partire dalla prima linea di base.

Mostra la differenza tra l'aggiunta di un normale spazio interno a un elemento
e l'applicazione di uno spazio interno alla linea di base di un elemento di testo.
Figura 1. Mostra la differenza tra l'aggiunta di un padding normale a un elemento e l'applicazione di un padding alla linea di base di un elemento Text.

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

Per leggere FirstBaseline nell'esempio, placeable [FirstBaseline] viene utilizzato nella fase di misurazione.

Creare linee di allineamento personalizzate

Quando crei un composable Layout personalizzato o un LayoutModifier personalizzato, puoi fornire linee di allineamento personalizzate in modo che altri composable principali possano utilizzarle per allineare e posizionare i relativi elementi secondari di conseguenza.

L'esempio seguente mostra un composable BarChart personalizzato che espone due linee di allineamento, MaxChartValue e MinChartValue, in modo che altri composable possano allinearsi al valore massimo e minimo dei dati del grafico. Due elementi di testo, Max e Min, sono stati allineati al centro delle linee di allineamento personalizzate.

Componente componibile BarChart con testo allineato al valore massimo e
minimo dei dati.
Figura 2. Composable BarChart con testo allineato al valore massimo e minimo dei dati.

Le linee di allineamento personalizzate sono definite come variabili di primo livello nel progetto.

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

Le linee di allineamento personalizzate per creare il nostro esempio sono di tipo HorizontalAlignmentLine, in quanto vengono utilizzate per allineare gli elementi secondari verticalmente. Viene trasmesso un criterio di unione come parametro nel caso in cui più layout forniscano un valore per queste linee di allineamento. Poiché il sistema di layout di Compose coordina e le Canvas coordinate rappresentano [0, 0], l'angolo in alto a sinistra e gli assi x e y sono positivi verso il basso, quindi il valore MaxChartValue sarà sempre inferiore a MinChartValue. Pertanto, il criterio di unione è min per la linea di base del valore massimo dei dati del grafico e max per la linea di base del valore minimo dei dati del grafico.

Quando crei un Layout o un LayoutModifier personalizzato, specifica le linee di allineamento personalizzate nel metodo MeasureScope.layout, che accetta un alignmentLines: Map<AlignmentLine, Int> parametro.

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

I genitori diretti e indiretti di questo composable possono utilizzare le linee di allineamento. Il composable seguente crea un layout personalizzato che accetta come parametro due slot Text e punti dati e allinea i due testi ai valori massimi e minimi dei dati del grafico. L'anteprima di questo composable è quella mostrata nella 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)
        )
    }
}