Compose fournit directement de nombreux modificateurs pour les comportements courants, mais vous pouvez également 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 pour votre modificateur et qui permet de l'associer facilement. La fabrique de modificateurs génère les éléments de modificateur utilisés par Compose pour modifier votre interface utilisateur.
- Il s'agit d'une fonction d'extension de
- 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 fonctionnalité requise. Souvent, le moyen le plus simple d'implémenter un modificateur personnalisé consiste simplement à implémenter une fabrique de modificateurs personnalisé qui combine d'autres fabriques de modificateurs déjà définies. Si vous avez besoin d'un comportement plus personnalisé, implémentez l'é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 simplement en utilisant des modificateurs existants. Par exemple, Modifier.clip()
est implémenté à l'aide du modificateur graphicsLayer
. Cette stratégie utilise des éléments de modificateur existants, et vous fournissez votre propre fabrique de modificateurs personnalisées.
Avant d'implémenter votre propre modificateur personnalisé, essayez d'utiliser la même stratégie.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Si vous constatez que vous répétez souvent le même groupe de modificateurs, vous pouvez les encapsuler 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 des API Compose de niveau supérieur, telles que animate*AsState
et d'autres API d'animation reposant sur l'état Compose. Par exemple, l'extrait suivant montre un modificateur qui anime un changement alpha lorsqu'il est activé ou 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 de l'implémenter consiste à utiliser une fabrique de modificateurs composables:
@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, les éléments locaux de composition récupèrent la valeur de l'arborescence de composition où ils sont créés et ne sont pas utilisés. Cela peut entraîner des résultats inattendus. Prenons l'exemple du modificateur local de composition ci-dessus, implémenté légèrement différemment à l'aide d'une fonction modulable:
@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 ce n'est pas ainsi que vous souhaitez que votre modificateur fonctionne, utilisez plutôt un Modifier.Node
personnalisé, car les conditions locales de composition seront correctement résolues sur le site d'utilisation et pourront être hissées 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 ayant des valeurs renvoyées ne peuvent pas être ignorées. Cela signifie que votre fonction de modification sera appelée à 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 la composition. Cette limite limite l'endroit où un modificateur peut être hissé, car il ne peut jamais être hissé hors de la composition. En comparaison, les fabriques de modificateurs non composables 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 s'agit de la même API que celle dans laquelle Compose implémente ses propres modificateurs et c'est le moyen le plus performant 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
qui contient la logique et l'état de votre modificateur. - Un
ModifierNodeElement
qui crée et met à jour les instances de nœud de modification. - Une fabrique de modificateurs facultative, comme indiqué ci-dessus.
Les classes ModifierNodeElement
sont sans état et de nouvelles instances sont allouées à chaque recomposition, tandis que les classes Modifier.Node
peuvent être avec état, survivre lors de plusieurs recompositions et peuvent même être réutilisées.
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
) met en œuvre les fonctionnalités 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 à la fonction de modificateur.
Un nœud implémente Modifier.Node
, ainsi que zéro, un ou plusieurs types de nœuds. Il existe différents types de nœuds en fonction des fonctionnalités requises par votre modificateur. L'exemple ci-dessus doit être capable de dessiner. Il implémente donc DrawModifierNode
, ce qui lui permet de remplacer la méthode de dessin.
Les types disponibles sont les suivants:
Nœud |
Utilisation |
Exemple de lien |
|
||
|
||
L'implémentation de cette interface permet à votre |
||
|
||
Un objet |
||
|
||
Un |
||
Un élément |
||
Les |
||
Un Cela peut être utile pour regrouper plusieurs implémentations de nœuds en une seule. |
||
Permet aux classes |
Les nœuds sont automatiquement invalidés lorsque la mise à jour est appelée sur leur élément correspondant. Comme notre exemple est un DrawModifierNode
, chaque mise à jour temporelle est appelée sur l'élément, le nœud déclenche une redessination et sa couleur est mise à jour correctement. 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 à la mise à jour de 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:
create
: il s'agit de la fonction qui instancie votre nœud de modificateur. Celui-ci est 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 transmis à la fabrique de modificateurs.update
: cette fonction est appelée chaque fois que ce modificateur est fourni au même endroit que ce nœud existe déjà, mais qu'une propriété a été modifiée. Ce paramètre est déterminé par la méthodeequals
de la classe. Le nœud de modificateur précédemment créé est envoyé en tant que paramètre à l'appelupdate
. À ce stade, vous devez mettre à jour les propriétés des nœuds pour qu'elles correspondent aux paramètres mis à jour. La possibilité de réutiliser les nœuds de cette manière est essentielle pour les gains de performances offerts parModifier.Node
. Par conséquent, vous devez mettre à jour le nœud existant plutôt que d'en créer un dans la méthodeupdate
. 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 "égal" à 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 permettent de vérifier si un nœud doit être mis à jour ou non. Si les propriétés de votre élément ne contribuent pas à la mise à jour d'un nœud ou si vous souhaitez éviter les classes de 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. Dans la plupart des cas, il suffit de créer l'élément modificateur et de l'ajouter à 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
Voici quelques situations courantes que vous pouvez rencontrer lorsque vous créez des modificateurs personnalisés avec Modifier.Node
.
Aucun paramètre
Si votre modificateur ne comporte aucun paramètre, il n'a jamais besoin d'être mis à jour et, en outre, n'a pas besoin d'être une classe de données. Voici un exemple d'implémentation d'un modificateur qui applique une marge intérieure fixe à 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 modifications apportées aux objets d'état Compose, tels que CompositionLocal
. L'avantage des modificateurs Modifier.Node
par rapport aux modificateurs qui viennent d'être créés avec une fabrique de composables est qu'ils peuvent lire la valeur de la composition locale à partir de l'endroit où le modificateur est utilisé dans l'arborescence de l'UI, et non à l'endroit où le modificateur est alloué, à l'aide de currentValueOf
.
Cependant, les instances de nœud modificateur n'observent pas automatiquement les changements d'état. Pour réagir automatiquement à la modification locale d'une composition, vous pouvez lire sa valeur actuelle dans un champ d'application:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
etIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
Cet exemple observe la valeur de LocalContentColor
pour dessiner un arrière-plan en fonction de sa couleur. Comme ContentDrawScope
observe les changements d'instantané, cette méthode 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 modificateur, utilisez un ObserverModifierNode
.
Par exemple, Modifier.scrollable
utilise cette technique pour observer les modifications apportées à 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 d'utiliser les API Compose Animatable. Par exemple, cet extrait modifie la CircleNode
ci-dessus pour effectuer un fondu à l'ouverture et à la fermeture 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. De nombreux cas d'utilisation permettent, par exemple, d'extraire des implémentations communes de différents modificateurs, mais il peut également être utilisé pour partager un état commun entre plusieurs modificateurs.
Par exemple, une implémentation de base d'un nœud modificateur cliquable qui partage des données 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 automatiquement invalidés lorsque leurs appels ModifierNodeElement
correspondants sont mis à jour. Dans un modificateur plus complexe, vous pouvez parfois être amené à désactiver ce comportement afin de contrôler plus précisément le moment où votre modificateur invalide les phases.
Cela peut être particulièrement utile si votre modificateur personnalisé modifie à la fois la mise en page et le dessin. Désactiver l'invalidation automatique vous permet d'invalider uniquement le dessin lorsque seules les propriétés liées au dessin, telles que color
, sont modifiées, sans invalider la mise en page.
Cela peut améliorer les performances de votre modificateur.
Vous trouverez ci-dessous un exemple hypothétique avec un modificateur ayant les propriétés lambda color
, size
et onClick
. Ce modificateur n'invalide que ce qui est requis 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) } } }