Créer des modificateurs personnalisés

Compose fournit directement de nombreux modificateurs pour les comportements courants, mais vous pouvez aussi créer vos propres modificateurs personnalisés.

Les modificateurs sont constitués de plusieurs parties:

  • Une fabrique de modificateurs
    • Il s'agit d'une fonction d'extension de Modifier, qui fournit une API idiomatique du modificateur et permet de l'enchaîner facilement. La fabrique de modificateurs génère les éléments de modificateur utilisés par Compose pour modifier votre interface utilisateur.
  • Un élément modificateur
    • C'est ici que vous pouvez implémenter le comportement de votre modificateur.

Il existe plusieurs façons d'implémenter un modificateur personnalisé en fonction de la et les fonctionnalités nécessaires. Souvent, le moyen le plus simple d'implémenter un modificateur personnalisé est pour implémenter une fabrique de modificateurs personnalisés qui combine d'autres des fabriques de modificateur. Si vous avez besoin d'un comportement plus personnalisé, implémentez la un élément modificateur à l'aide des API Modifier.Node, qui sont de niveau inférieur, mais offrent plus de flexibilité.

Chaîner des modificateurs existants

Il est souvent possible de créer des modificateurs personnalisés en utilisant simplement modificateurs de requête large. Par exemple, Modifier.clip() est implémenté à l'aide de la Modificateur graphicsLayer. Cette stratégie utilise des éléments modificateurs existants. votre propre fabrique de modificateurs personnalisées.

Avant d'implémenter votre propre modificateur personnalisé, voyez s'il est possible de l'utiliser stratégie.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Ou, si vous constatez que vous répétez souvent le même groupe de modificateurs, vous pouvez encapsulez-les dans votre propre modificateur:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Créer un modificateur personnalisé à l'aide d'une fabrique de modificateurs de composables

Vous pouvez également créer un modificateur personnalisé à l'aide d'une fonction composable pour transmettre des valeurs. à un modificateur existant. C'est ce qu'on appelle une fabrique de modificateurs de composables.

L'utilisation d'une fabrique de modificateurs composables pour créer un modificateur permet également d'utiliser API Compose de niveau supérieur, telles que animate*AsState et d'autres API Compose API d'animation reposant sur l'état. Par exemple, l'extrait suivant montre une modificateur qui anime un changement alpha lorsqu'il est activé/désactivé:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

Si votre modificateur personnalisé est une méthode pratique permettant de fournir des valeurs par défaut à partir d'un CompositionLocal, le moyen le plus simple d'implémenter cela consiste à utiliser un composable. fabrique de modificateurs:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Cette approche comporte certaines mises en garde détaillées ci-dessous.

Les valeurs CompositionLocal sont résolues sur le site d'appel de la fabrique de modificateurs.

Lors de la création d'un modificateur personnalisé à l'aide d'une fabrique de modificateurs de composables, la composition les locaux tirent la valeur de l'arborescence de composition où ils sont créés, et non utilisé. Cela peut entraîner des résultats inattendus. Par exemple, prenons la composition l'exemple de modificateur local ci-dessus, implémenté de manière légèrement différente à l'aide d'un fonction composable:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

Si votre modificateur ne fonctionne pas ainsi, utilisez une Modifier.Node à la place, car les compositions locales seront correctement résolus sur le site d'utilisation et peuvent être hissés en toute sécurité.

Les modificateurs de fonction modulables ne sont jamais ignorés

Les modificateurs de fabrique composables ne sont jamais ignorés, car les fonctions composables contenant des valeurs renvoyées ne peuvent pas être ignorées. Cela signifie que votre fonction de modificateur sera appelé à chaque recomposition, ce qui peut s'avérer coûteux si elle se recompose fréquemment.

Les modificateurs de fonction modulables doivent être appelés dans une fonction modulable

Comme toutes les fonctions composables, un modificateur de fabrique de composables doit être appelé à partir de dans la composition. Cela permet de limiter l'endroit où un modificateur peut être hissé, ne jamais être hissés de la composition. En comparaison, le modificateur non modulable les fabriques peuvent être extraites des fonctions composables pour faciliter la réutilisation et améliorer les performances:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

Implémenter le comportement du modificateur personnalisé à l'aide de Modifier.Node

Modifier.Node est une API de niveau inférieur permettant de créer des modificateurs dans Compose. Il est la même API que celle dans laquelle Compose implémente ses propres modificateurs. C'est la efficace de créer des modificateurs personnalisés.

Implémenter un modificateur personnalisé à l'aide de Modifier.Node

L'implémentation d'un modificateur personnalisé à l'aide de Modifier.Node s'effectue en trois étapes:

  • Une implémentation Modifier.Node contenant la logique et de votre modificateur.
  • Un élément ModifierNodeElement qui crée et met à jour le modificateur instances de nœud.
  • Une fabrique de modificateurs facultative, comme indiqué ci-dessus.

Les classes ModifierNodeElement sont sans état, et les nouvelles instances sont allouées chacune la recomposition, tandis que les classes Modifier.Node peuvent être avec état et survivre. lors de plusieurs recompositions et peut même être réutilisé.

La section suivante décrit chaque partie et montre un exemple de création d'un modificateur personnalisé pour dessiner un cercle.

Modifier.Node

L'implémentation de Modifier.Node (dans cet exemple, CircleNode) implémente la de votre modificateur personnalisé.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Dans cet exemple, il dessine le cercle avec la couleur transmise au modificateur. .

Un nœud implémente Modifier.Node, ainsi que zéro, un ou plusieurs types de nœuds. Il y a différents types de nœuds selon les fonctionnalités requises par votre modificateur. La L'exemple ci-dessus doit être capable de dessiner. Il implémente donc DrawModifierNode, ce qui permet de remplacer la méthode de dessin.

Les types disponibles sont les suivants:

Nœud

Utilisation

Exemple de lien

LayoutModifierNode

Modifier.Node qui modifie la façon dont son contenu encapsulé est mesuré et présenté.

Exemple

DrawModifierNode

Modifier.Node qui s'affiche dans l'espace de la mise en page.

Exemple

CompositionLocalConsumerModifierNode

L'implémentation de cette interface permet à votre Modifier.Node de lire la composition locale.

Exemple

SemanticsModifierNode

Modifier.Node qui ajoute une clé/valeur sémantique à utiliser lors des tests, de l'accessibilité et des cas d'utilisation similaires.

Exemple

PointerInputModifierNode

Un objet Modifier.Node qui reçoit PointerInputChanges.

Exemple

ParentDataModifierNode

Modifier.Node qui fournit des données à la mise en page parente.

Exemple

LayoutAwareModifierNode

Un Modifier.Node qui reçoit des rappels onMeasured et onPlaced.

Exemple

GlobalPositionAwareModifierNode

Un élément Modifier.Node qui reçoit un rappel onGloballyPositioned avec le LayoutCoordinates final de la mise en page lorsque la position globale du contenu a peut-être changé.

Exemple

ObserverModifierNode

Les Modifier.Node qui implémentent ObserverNode peuvent fournir leur propre implémentation de onObservedReadsChanged, qui sera appelée en réponse aux modifications apportées aux objets d'instantané lus dans un bloc observeReads.

Exemple

DelegatingNode

Un Modifier.Node capable de déléguer des tâches à d'autres instances Modifier.Node.

Cela peut être utile pour regrouper plusieurs implémentations de nœuds en une seule.

Exemple

TraversableNode

Permet aux classes Modifier.Node de balayer l'arborescence des nœuds vers le haut/bas pour les classes du même type ou pour une clé particulière.

Exemple

Les nœuds sont automatiquement invalidés lorsque la mise à jour est appelée sur leur . Comme notre exemple est un DrawModifierNode, n'importe quelle mise à jour de l'heure est appelée l'élément, le nœud déclenche un redessin et sa couleur est mise à jour correctement. Il est vous pouvez désactiver l'invalidation automatique comme indiqué ci-dessous.

ModifierNodeElement

Un ModifierNodeElement est une classe immuable qui contient les données nécessaires à la création ou Mettez à jour votre modificateur personnalisé:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

Les implémentations de ModifierNodeElement doivent remplacer les méthodes suivantes:

  1. create: il s'agit de la fonction qui instancie votre nœud de modificateur. Cela obtient appelé pour créer le nœud lorsque votre modificateur est appliqué pour la première fois. En général, cela revient à construire le nœud et à le configurer avec les paramètres ont été transmises à la fabrique de modificateurs.
  2. update: cette fonction est appelée chaque fois que ce modificateur est fourni dans le au même endroit que ce nœud existe déjà, mais une propriété a été modifiée. C'est déterminé par la méthode equals de la classe. Le nœud modificateur qui était créé précédemment est envoyé en tant que paramètre à l'appel update. À ce stade, vous devez mettre à jour pour qu'elles correspondent aux nouvelles paramètres. La possibilité de réutiliser les nœuds de cette manière est gains de performances qu'apporte Modifier.Node ; Vous devez donc mettre à jour un nœud existant plutôt que d'en créer un dans la méthode update. Dans notre exemple de cercle, la couleur du nœud est mise à jour.

De plus, les implémentations ModifierNodeElement doivent également implémenter equals. et hashCode. update n'est appelé que si une comparaison "est égal(e) à" avec la l'élément précédent renvoie la valeur "false".

L'exemple ci-dessus utilise une classe de données à cette fin. Ces méthodes sont utilisées pour vérifier si un nœud doit être mis à jour ou non. Si les propriétés de votre élément ne contribue pas à déterminer si un nœud doit être mis à jour, ou si vous souhaitez éviter que des données pour des raisons de compatibilité binaire, vous pouvez implémenter manuellement equals et hashCode (par exemple, L'élément modificateur de marge intérieure.

Fabrique de modificateurs

Il s'agit de la surface d'API publique de votre modificateur. La plupart des implémentations Créez l'élément modificateur et ajoutez-le à la chaîne de modificateur:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

Exemple complet

Ces trois parties sont réunies pour créer le modificateur personnalisé permettant de dessiner un cercle à l'aide des API Modifier.Node:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Cas courants d'utilisation de Modifier.Node

Lorsque vous créez des modificateurs personnalisés avec Modifier.Node, voici quelques situations courantes dans lesquelles vous pouvez rencontrer.

Aucun paramètre

Si votre modificateur ne comporte aucun paramètre, il n'a jamais besoin d'être mis à jour. qui n'a pas besoin d'être une classe de données. Voici un exemple d'implémentation d'un modificateur qui applique une quantité fixe de marge intérieure à un composable:

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            placeable.place(paddingPx, paddingPx)
        }
    }
}

Référencer la composition locale

Les modificateurs Modifier.Node n'observent pas automatiquement les changements d'état de Compose des objets, tels que CompositionLocal. L'avantage des modificateurs Modifier.Node par rapport à les modificateurs créés avec une fabrique de composables, c'est qu'ils peuvent lire Valeur de la composition locale à partir de laquelle le modificateur est utilisé dans votre UI et non là où le modificateur est alloué, à l'aide de currentValueOf.

Cependant, les instances de nœud modificateur n'observent pas automatiquement les changements d'état. À réagissent automatiquement à la modification locale d'une composition, vous pouvez lire dans un champ d'application:

Cet exemple observe la valeur de LocalContentColor pour dessiner un arrière-plan basé sur sur sa couleur. Comme ContentDrawScope observe les changements d'instantané, cette se redessine automatiquement lorsque la valeur de LocalContentColor change:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Pour réagir aux changements d'état en dehors d'un champ d'application et mettre à jour automatiquement votre utilisez un modificateur ObserverModifierNode.

Par exemple, Modifier.scrollable utilise cette technique pour observer les changements dans LocalDensity. Voici un exemple simplifié:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // Place holder fling behavior, we'll initialize it when the density is available.
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // monitor change in Density
    }

    override fun onObservedReadsChanged() {
        // if density changes, update the default fling behavior.
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

Modificateur d'animation

Les implémentations Modifier.Node ont accès à un coroutineScope. Cela permet Utilisation des API Compose Animatable Par exemple, cet extrait modifie le CircleNode d'en haut pour apparaître et disparaître en fondu de manière répétée:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private val alpha = Animatable(1f)

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

Partager l'état entre des modificateurs à l'aide de la délégation

Les modificateurs Modifier.Node peuvent déléguer à d'autres nœuds. Il existe de nombreux cas d'utilisation comme extraire des implémentations communes à différents modificateurs, mais il peut aussi être utilisé pour partager un état commun entre des modificateurs.

Par exemple, l'implémentation de base d'un nœud modificateur cliquable qui partage d'interaction:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Désactiver l'invalidation automatique des nœuds

Les nœuds Modifier.Node sont invalidés automatiquement lorsque leur ModifierNodeElement appels mis à jour. Parfois, dans un modificateur plus complexe, vous pouvez Vous souhaitez désactiver ce comportement afin de contrôler plus précisément votre modificateur invalide les phases.

Cela peut être particulièrement utile si votre modificateur personnalisé modifie à la fois la mise en page et dessiner. La désactivation de l'invalidation automatique vous permet d'invalider simplement le dessin Seules les propriétés liées au dessin, comme color, peuvent être modifiées, mais ne pas invalider la mise en page. Cela peut améliorer les performances de votre modificateur.

Vous trouverez ci-dessous un exemple fictif avec un modificateur ayant une color, size et le lambda onClick comme propriétés. Ce modificateur n'invalide que ce qui est obligatoire et ignore toute invalidation qui n'est pas:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
    override val shouldAutoInvalidate: Boolean
        get() = false

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // Only invalidate draw when color changes
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // Only invalidate layout when size changes
            invalidateMeasurement()
        }

        // If only onClick changes, we don't need to invalidate anything
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}