Mesures intrinsèques dans les mises en page Compose

L'une des règles de Compose est de ne mesurer vos éléments enfants qu'une seule fois. Si vous les mesurez deux fois, une exception d'exécution est générée. Toutefois, il arrive que vous ayez besoin d'informations sur vos éléments enfants avant de les mesurer.

Les fonctionnalités intrinsèques vous permettent d'interroger des éléments enfants avant qu'ils ne soient réellement mesurés.

Dans le cas d'un composable, vous pouvez demander son intrinsicWidth ou intrinsicHeight :

  • (min|max)IntrinsicWidth: compte tenu de cette largeur, quelle est la largeur minimale/maximale de votre contenu que vous pouvez peindre correctement ?
  • (min|max)IntrinsicHeight: compte tenu de cette hauteur, quelle est la hauteur minimale/maximale de votre contenu que vous pouvez peindre correctement ?

Par exemple, si vous demandez la minIntrinsicHeight d'un Text avec une height infinie, la height du Text est renvoyée comme si le texte était dessiné avec une seule ligne.

Fonctionnalités intrinsèques en action

Supposons que vous souhaitiez créer un composable qui affiche à l'écran deux éléments textuels séparés comme ceci :

Deux éléments de texte côte à côte, séparés par une ligne verticale

Comment allez-vous procéder ? Il peut y avoir une Row contenant deux éléments Text qui s'étendent autant que possible et, au milieu, un Divider. Nous voulons que l'Divider soit aussi grand que l'Text le plus grand et fin (width = 1.dp).

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        HorizontalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

En affichant l'aperçu, on constate que Divider s'étend sur toute la hauteur de l'écran. Ce n'est pas ce qui était prévu:

Deux éléments de texte côte à côte, séparés par un séparateur, mais le séparateur s'étend sous le bas du texte

Cela est dû au fait que Row mesure chaque élément enfant séparément et que la hauteur de Text ne peut pas être utilisée pour limiter le Divider. L'objectif est que Divider remplisse l'espace disponible avec une hauteur donnée. Pour cela, vous pouvez utiliser le modificateur height(IntrinsicSize.Min).

height(IntrinsicSize.Min) dimensionne ses éléments enfants en les forçant à être aussi grands que leur hauteur intrinsèque minimale. Compte tenu de sa nature récursive, il va interroger Row et la minIntrinsicHeight de ses éléments enfants.

Si l'on applique cela à notre code, on obtiendra le résultat attendu :

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        HorizontalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

// @Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

Avec l'aperçu :

Deux éléments de texte côte à côte, séparés par une ligne verticale

La propriété minIntrinsicHeight du composable Row correspondra à la valeur minIntrinsicHeight maximale de ses éléments enfants. La propriété minIntrinsicHeight de l'élément Divider est 0, car elle n'occupe pas d'espace si aucune contrainte n'est spécifiée. La propriété minIntrinsicHeight de Text correspondra à celle du texte auquel une width spécifique est donnée. Par conséquent, la contrainte height de l'élément Row correspond à la valeur minIntrinsicHeight maximale des éléments Text. Divider va alors étendre sa height en fonction de la contrainte height fournie par Row.

Fonctionnalités intrinsèques dans vos mises en page personnalisées

Lorsque vous créez un modificateur Layout ou layout personnalisé, les mesures intrinsèques sont calculées automatiquement en fonction d'approximations. Par conséquent, les calculs ne seront peut-être pas corrects pour toutes les mises en page. Ces API offrent des options pour remplacer ces valeurs par défaut.

Pour spécifier les mesures intrinsèques de votre Layout personnalisée, remplacez les valeurs minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth et maxIntrinsicHeight de l'interface MeasurePolicy lors de sa création.

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // Measure and layout here
                // ...
            }

            override fun IntrinsicMeasureScope.minIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ): Int {
                // Logic here
                // ...
            }

            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )
}

Lorsque vous créez votre modificateur layout personnalisé, remplacez les méthodes associées dans l'interface LayoutModifier.

fun Modifier.myCustomModifier(/* ... */) = this then object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
        // ...
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int {
        // Logic here
        // ...
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
}