Compose fournit de nombreux modificateurs pour les comportements courants prêts à l'emploi, mais vous pouvez également créer vos propres modificateurs personnalisés.
Les modificateurs comportent plusieurs parties :
- Une fabrique de modificateurs
- Il s'agit d'une fonction d'extension sur
Modifier, qui fournit une API idiomatique pour votre modificateur et permet d'enchaîner les modificateurs. La fabrique de modificateurs produit les éléments de modificateur utilisés par Compose pour modifier votre interface utilisateur.
- Il s'agit d'une fonction d'extension sur
- Un élément de modificateur
- C'est là que vous pouvez implémenter le comportement de votre modificateur.
Il existe plusieurs façons d'implémenter un modificateur personnalisé en fonction des fonctionnalités nécessaires. Souvent, le moyen le plus simple d'implémenter un modificateur personnalisé consiste à implémenter une fabrique de modificateurs personnalisés 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 de modificateur à l'aide des API Modifier.Node, qui sont de niveau inférieur, mais offrent plus de flexibilité.
Enchaîner des modificateurs existants
Il est souvent possible de créer des modificateurs personnalisés en utilisant des modificateurs existants. Par
exemple, Modifier.clip() est implémenté à l'aide du graphicsLayer
modificateur. Cette stratégie utilise des éléments de modificateur existants, et vous fournissez votre propre fabrique de modificateurs personnalisés.
Avant d'implémenter votre propre modificateur personnalisé, vérifiez si vous pouvez 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 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 composables.
L'utilisation d'une fabrique de modificateurs composables pour créer un modificateur vous permet également d'utiliser
des API Compose de niveau supérieur, telles que animate*AsState et d'autres Compose
API d'animation Compose reposant sur l'état. Par exemple, l'extrait suivant montre un modificateur qui anime un changement d'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 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 présente quelques inconvénients, qui sont détaillés dans les sections suivantes.
Les valeurs CompositionLocal sont résolues sur le site d'appel de la fabrique de modificateurs
Lorsque vous créez un modificateur personnalisé à l'aide d'une fabrique de modificateurs composables, les éléments de composition locaux prennent leur valeur à partir de l'arborescence de composition où ils sont créés, et non de celle où ils sont utilisés. Cela peut entraîner des résultats inattendus. Par exemple, prenons l'exemple de modificateur local de composition mentionné précédemment, implémenté légèrement différemment à l'aide d'une 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 ce n'est pas le comportement attendu pour votre modificateur, utilisez plutôt un
Modifier.Node personnalisé, car les éléments de composition locaux seront
correctement résolus sur le site d'utilisation et pourront être remontés en toute sécurité.
Les modificateurs de fonction composable ne sont jamais ignorés
Les modificateurs de fabrique composables ne sont jamais ignorés, car les fonctions composables qui ont des valeurs de retour ne peuvent pas être ignorées. Cela signifie que votre fonction de modificateur sera appelée à chaque recomposition, ce qui peut être coûteux si elle est recomposée fréquemment.
Les modificateurs de fonction composable doivent être appelés dans une fonction composable
Comme toutes les fonctions composables, un modificateur de fabrique composable doit être appelé depuis la composition. Cela limite l'emplacement où un modificateur peut être remonté, car il ne peut jamais être remonté hors de la composition. En comparaison, les fabriques de modificateurs non composables peuvent être remontées en dehors 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 un comportement de 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 également de l'API dans laquelle Compose implémente ses propres modificateurs. C'est la méthode la plus performante pour 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 comporte trois parties :
- Une
Modifier.Nodeimplémentation qui contient la logique et l'état de votre modificateur. - Un
ModifierNodeElementqui crée et met à jour les instances de nœud de modificateur. - Une fabrique de modificateurs facultative, comme indiqué précédemment.
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 à plusieurs recompositions, et 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
Modifier.Node (CircleNode dans cet exemple) implémente la fonctionnalité de votre modificateur personnalisé.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Dans cet exemple, elle dessine le cercle avec la couleur transmise à la fonction de modificateur.
Un nœud implémente Modifier.Node, ainsi que zéro 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 précédent doit pouvoir 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 |
Lien vers l'exemple |
Un |
||
Un |
||
L'implémentation de cette interface permet à votre |
||
Un |
||
Un |
||
Un |
||
Un |
||
Un |
||
Les |
||
Un Cela peut être utile pour composer plusieurs implémentations de nœuds en une seule. |
||
Permet aux classes |
Les nœuds sont automatiquement invalidés lorsqu'une mise à jour est appelée sur l'élément correspondant. Comme notre exemple est un DrawModifierNode, chaque fois que la mise à jour de l'heure est appelée sur l'élément, le nœud déclenche une nouvelle action de dessin et sa couleur est correctement mise à jour. Il
est possible de désactiver l'invalidation automatique, comme indiqué dans la
section Désactiver l'invalidation automatique des nœuds.
ModifierNodeElement
Un ModifierNodeElement est une classe immuable qui contient les données permettant de créer ou de mettre à 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 :
create: il s'agit de la fonction qui instancie votre nœud de modificateur. Elle est appelée pour créer le nœud lorsque votre modificateur est appliqué pour la première fois. En règle générale, cela revient à construire le nœud et à le configurer avec les paramètres qui ont été transmis à la fabrique de modificateurs.update: cette fonction est appelée chaque fois que ce modificateur est fourni à l'emplacement où ce nœud existe déjà, mais qu'une propriété a changé. Cela est déterminé par la méthodeequalsde la classe. Le nœud de modificateur créé précédemment est envoyé en tant que paramètre à l'appelupdate. À ce stade, vous devez mettre à jour les propriétés du nœud pour qu'elles correspondent aux paramètres modifiés. La possibilité de réutiliser les nœuds de cette manière est essentielle pour les gains de performances apportés parModifier.Node. Vous devez donc mettre le nœud à jour avec la méthodeupdateau lieu d'en créer un autre. Dans notre exemple de cercle, la couleur du nœud est mise à jour.
De plus, les implémentations de ModifierNodeElement doivent également implémenter equals et hashCode. update ne sera appelé que si une comparaison d'égalité avec l'élément précédent renvoie la valeur "false".
L'exemple précédent utilise une classe de données pour y parvenir. Ces méthodes permettent de vérifier si un nœud doit être mis à jour ou non. Si votre élément possède des propriétés qui
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 de modificateur de marge intérieure
.
Fabrique de modificateurs
Il s'agit de la surface de l'API publique de votre modificateur. La plupart des implémentations créent l'élément de modificateur et l'ajoutent à la chaîne de modificateurs :
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Exemple complet
Ces trois parties se combinent 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) } }
Situations courantes utilisant 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 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 les éléments de composition locaux
Les modificateurs Modifier.Node n'observent pas automatiquement les modifications apportées aux objets d'état Compose, comme CompositionLocal. L'avantage des modificateurs Modifier.Node par rapport à ceux qui sont simplement créés avec une fabrique de composables est qu'ils peuvent lire la valeur de l'élément de composition local là où le modificateur est utilisé dans votre arborescence d'UI, à l'aide de currentValueOf, et non à l'endroit où il est alloué.
Toutefois, les instances de nœud de modificateur n'observent pas automatiquement les changements d'état. Pour réagir automatiquement à la modification d'un élément de composition local, vous pouvez lire sa valeur actuelle dans une plage donnée :
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScopeetIntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
Cet exemple observe la valeur de LocalContentColor pour dessiner un arrière-plan en fonction de sa couleur. Comme ContentDrawScope observe les modifications apportées aux instantanés, il 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'une plage et mettre à jour automatiquement votre modificateur, utilisez un ObserverModifierNode.
Par exemple, Modifier.scrollable utilise cette technique pour
observer les modifications apportées à LocalDensity. L'exemple suivant en présente une version simplifiée :
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) } }
Animer un modificateur
Les implémentations de Modifier.Node ont accès à un coroutineScope. Cela permet
d'utiliser les API Animatable de Compose. Par exemple, cet extrait modifie le CircleNode présenté précédemment pour qu'il s'affiche et disparaisse de manière répétée :
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private lateinit var alpha: Animatable<Float, AnimationVector1D> override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Partager l'état entre les 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 pour cela, comme l'extraction d'implémentations courantes dans différents modificateurs, mais la délégation peut aussi être utilisée pour partager un état commun entre les modificateurs.
Par exemple, une implémentation de base d'un nœud de 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. Pour les modificateurs complexes, vous pouvez désactiver ce comportement afin de contrôler plus précisément le moment où votre modificateur invalide les phases.
Cela est particulièrement utile si votre modificateur personnalisé modifie à la fois la mise en page et le dessin. Si vous désactivez l'invalidation automatique, vous pouvez simplement invalider le dessin et non la mise en page, par exemple lorsque seules les propriétés liées au dessin changent, comme color. Cela évite d'invalider la mise en page et peut améliorer les performances de votre modificateur.
L'exemple suivant présente un exemple hypothétique avec un modificateur dont les propriétés sont un lambda color, size et onClick. Ce modificateur n'invalide que ce qui est nécessaire, en ignorant toute invalidation inutile :
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) } } }