Lignes d'alignement dans Jetpack Compose

Le modèle de mise en page Compose vous permet d'utiliser la ligne d'alignement (AlignmentLine) pour créer des lignes d'alignement personnalisées que les mises en page parentes pourront utiliser pour aligner et positionner leurs enfants. Par exemple, Row (ligne) peut utiliser les lignes d'alignement personnalisées de ses enfants pour les aligner.

Lorsqu'une mise en page fournit une valeur pour uneAlignmentLine spécifique, les parents de la mise en page peuvent lire cette valeur après la mesure en utilisant l'opérateur Placeable.get sur l'instance Placeable correspondante. En fonction de la position d'AlignmentLine, les parents peuvent ensuite décider du positionnement des enfants.

Certains composables dans Compose sont déjà dotés de lignes d'alignement. Par exemple, le composable BasicText expose les lignes d'alignement FirstBaseline et LastBaseline.

Dans l'exemple ci-dessous, un modificateur de mise en page (LayoutModifier) personnalisé nommé firstBaselineToTop lit la première ligne de base (FirstBaseline) pour ajouter une marge intérieure au texte (Text) à partir de sa première ligne de base.

Figure 1 : Indique la différence entre l'ajout d'une marge intérieure normale à un élément et l'application d'une marge intérieure à la première ligne de base d'un élément textuel.

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

Pour lire la FirstBaseline dans l'exemple, placeable [FirstBaseline] est utilisé dans la phase de mesure.

Créer des lignes d'alignement personnalisées

Lorsque vous créez un composable Layout ou LayoutModifier personnalisé, vous pouvez fournir des lignes d'alignement personnalisées afin que d'autres composables parents puissent les utiliser pour aligner et positionner leurs enfants.

L'exemple suivant montre un composable BarChart personnalisé qui expose deux lignes d'alignement, MaxChartValue et MinChartValue, afin que d'autres composables puissent s'aligner sur les valeurs maximale et minimale des données du graphique. Deux éléments de texte, Max et Min, ont été alignés au centre des lignes d'alignement personnalisées.

Figure 2. Composable BarChart avec texte aligné sur les valeurs maximale et minimale des données.

Les lignes d'alignement personnalisées sont définies comme des variables de niveau supérieur dans votre projet.

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

Les lignes d'alignement personnalisées utilisées pour notre exemple sont de type HorizontalAlignmentLine, car elles permettent d'aligner verticalement les enfants. Une règle de fusion est transmise en tant que paramètre si plusieurs mises en page fournissent une valeur pour ces lignes d'alignement. Puisque les coordonnées du système de mise en page Compose et celle de Canvas représentent [0, 0], le coin supérieur gauche et les axes x ety sont des valeurs positives vers le bas de sorte que la valeur MaxChartValue sera toujours inférieure à celle de MinChartValue. Par conséquent, la règle de fusion est min pour la valeur de référence maximale des données de graphique et max pour la valeur de référence minimale.

Lorsque vous créez une Layout ou un LayoutModifier personnalisé, spécifiez des lignes d'alignement personnalisées dans la méthode MeasureScope.layout, qui accepte un paramètre 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()
                        )
                    ) {}
                }
            }
        }
    }
}

Les parents directs et indirects de ce composable peuvent utiliser les lignes d'alignement. Le composable suivant crée une mise en page personnalisée qui utilise comme paramètre deux emplacements Text et points de données, et aligne les deux textes avec les valeurs maximales et minimales des données de graphique. L'aperçu de ce composable est illustré dans la Figure 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)
        )
    }
}