Compose fornisce molti modificatori per i comportamenti comuni pronti all'uso, ma puoi anche creare modificatori personalizzati.
I modificatori hanno più parti:
- Una factory di modificatori
- Si tratta di una funzione di estensione su
Modifier, che fornisce un'API idiomatica per il modificatore e consente di concatenare i modificatori. La factory di modificatori produce gli elementi modificatori utilizzati da Compose per modificare l'UI.
- Si tratta di una funzione di estensione su
- Un elemento modificatore
- Qui puoi implementare il comportamento del modificatore.
Esistono diversi modi per implementare un modificatore personalizzato a seconda della funzionalità necessaria. Spesso, il modo più semplice per implementare un modificatore personalizzato è implementare una factory di modificatori personalizzata che combina altre factory di modificatori già definite. Se hai bisogno di un comportamento più personalizzato, implementa l'elemento modificatore utilizzando le API Modifier.Node, che sono di livello inferiore ma offrono maggiore flessibilità.
Concatenare i modificatori esistenti
Spesso è possibile creare modificatori personalizzati utilizzando i modificatori esistenti. Ad
esempio, Modifier.clip() viene implementato utilizzando il graphicsLayer
modificatore. Questa strategia utilizza gli elementi modificatori esistenti e tu fornisci la tua factory di modificatori personalizzata.
Prima di implementare il tuo modificatore personalizzato, verifica se puoi utilizzare la stessa strategia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
In alternativa, se noti di ripetere spesso lo stesso gruppo di modificatori, puoi inserirli nel tuo modificatore:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Creare un modificatore personalizzato utilizzando una factory di modificatori composable
Puoi anche creare un modificatore personalizzato utilizzando una funzione composable per passare i valori a un modificatore esistente. Questa operazione è nota come factory di modificatori composable.
L'utilizzo di una factory di modificatori composable per creare un modificatore consente anche di utilizzare
API Compose di livello superiore, come animate*AsState e altre API di animazione supportate dallo stato di
Compose. Ad esempio, lo snippet seguente mostra un modificatore che anima una modifica alfa quando è attivato/disattivato:
@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 } }
Se il modificatore personalizzato è un metodo pratico per fornire valori predefiniti da un CompositionLocal, il modo più semplice per implementarlo è utilizzare una factory di modificatori composable:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Questo approccio presenta alcune limitazioni, descritte in dettaglio nelle sezioni seguenti.
I valori CompositionLocal vengono risolti nel sito di chiamata della factory di modificatori
Quando crei un modificatore personalizzato utilizzando una factory di modificatori composable, le composizioni locali prendono il valore dall'albero di composizione in cui vengono create, non utilizzate. Questo può portare a risultati imprevisti. Ad esempio, considera l'esempio di modificatore locale di composizione menzionato in precedenza, implementato in modo leggermente diverso utilizzando una funzione 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) } } }
Se non è così che ti aspetti che funzioni il modificatore, utilizza invece un
Modifier.Node personalizzato, poiché le composizioni locali verranno
risolte correttamente nel sito di utilizzo e possono essere sollevate in sicurezza.
I modificatori delle funzioni composable non vengono mai ignorati
I modificatori delle factory composable non vengono mai ignorati perché le funzioni composable che hanno valori restituiti non possono essere ignorate. Ciò significa che la funzione modificatore verrà chiamata a ogni ricomposizione, il che potrebbe essere costoso se la ricomposizione è frequente.
I modificatori delle funzioni composable devono essere chiamati all'interno di una funzione composable
Come tutte le funzioni composable, un modificatore di factory composable deve essere chiamato dall'interno della composizione. Ciò limita la posizione in cui un modificatore può essere sollevato, poiché non può mai essere sollevato dalla composizione. Al contrario, le factory di modificatori non composable possono essere sollevate dalle funzioni composable per consentire un riutilizzo più semplice e migliorare il rendimento:
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 }
Implementare il comportamento del modificatore personalizzato utilizzando Modifier.Node
Modifier.Node è un'API di livello inferiore per la creazione di modificatori in Compose. È la stessa API in cui Compose implementa i propri modificatori ed è il modo più efficiente per creare modificatori personalizzati.
Implementare un modificatore personalizzato utilizzando Modifier.Node
L'implementazione di un modificatore personalizzato utilizzando Modifier.Node è composta da tre parti:
- Un'implementazione di
Modifier.Nodeche contiene la logica e lo stato del modificatore. - Un
ModifierNodeElementche crea e aggiorna le istanze dei nodi modificatori. - Una factory di modificatori facoltativa, come descritto in precedenza.
Le classi ModifierNodeElement non hanno stato e le nuove istanze vengono allocate a ogni ricomposizione, mentre le classi Modifier.Node possono avere stato, sopravvivono a più ricomposizioni e possono persino essere riutilizzate.
La sezione seguente descrive ogni parte e mostra un esempio di creazione di un modificatore personalizzato per disegnare un cerchio.
Modifier.Node
L'implementazione di Modifier.Node (in questo esempio, CircleNode) implementa la funzionalità del modificatore personalizzato.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
In questo esempio, disegna il cerchio con il colore passato alla funzione modificatore.
Un nodo implementa Modifier.Node e zero o più tipi di nodi. Esistono diversi tipi di nodi in base alla funzionalità richiesta dal modificatore. L'esempio precedente deve essere in grado di disegnare, quindi implementa DrawModifierNode, che gli consente di sostituire il metodo di disegno.
I tipi disponibili sono i seguenti:
Nodo |
Utilizzo |
Esempio di link |
Un |
||
Un |
||
L'implementazione di questa interfaccia consente al tuo |
||
Un |
||
Un |
||
Un |
||
Un |
||
Un |
||
I |
||
Un Questa funzionalità può essere utile per combinare più implementazioni di nodi in una sola. |
||
Consente alle classi |
I nodi vengono invalidati automaticamente quando viene chiamato l'aggiornamento sull'elemento corrispondente. Poiché il nostro esempio è un DrawModifierNode, ogni volta che viene chiamato l'aggiornamento sull'elemento, il nodo attiva un ridisegno e il colore viene aggiornato correttamente. È possibile disattivare l'invalidazione automatica, come descritto nella sezione
Disattivare l'invalidazione automatica dei nodi.
ModifierNodeElement
Un ModifierNodeElement è una classe immutabile che contiene i dati per creare o aggiornare il modificatore personalizzato:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
Le implementazioni di ModifierNodeElement devono sostituire i seguenti metodi:
create: questa è la funzione che crea un'istanza del nodo modificatore. Viene chiamata per creare il nodo quando il modificatore viene applicato per la prima volta. In genere, questa operazione consiste nel creare il nodo e configurarlo con i parametri passati alla factory di modificatori.update: questa funzione viene chiamata ogni volta che questo modificatore viene fornito nella stessa posizione in cui esiste già questo nodo, ma una proprietà è cambiata. Questa operazione è determinata dal metodoequalsdella classe. Il nodo modificatore creato in precedenza viene inviato come parametro alla chiamataupdate. A questo punto, devi aggiornare le proprietà dei nodi in modo che corrispondano ai parametri aggiornati. La possibilità di riutilizzare i nodi in questo modo è fondamentale per i miglioramenti del rendimento offerti daModifier.Node; pertanto, devi aggiornare il nodo esistente anziché crearne uno nuovo nel metodoupdate. Nel nostro esempio di cerchio, il colore del nodo viene aggiornato.
Inoltre, le implementazioni di ModifierNodeElement devono implementare anche equals e hashCode. update verrà chiamato solo se un confronto di uguaglianza con l'elemento precedente restituisce false.
L'esempio precedente utilizza una classe di dati per ottenere questo risultato. Questi metodi vengono utilizzati per verificare se un nodo deve essere aggiornato o meno. Se l'elemento ha proprietà che non contribuiscono a determinare se un nodo deve essere aggiornato o se vuoi evitare le classi di dati per motivi di compatibilità binaria, puoi implementare manualmente equals e hashCode, ad esempio l'elemento modificatore di padding.
Factory di modificatori
Questa è la superficie dell'API pubblica del modificatore. La maggior parte delle implementazioni crea l'elemento modificatore e lo aggiunge alla catena di modificatori:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Esempio completo
Queste tre parti si combinano per creare il modificatore personalizzato per disegnare un cerchio utilizzando le 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) } }
Situazioni comuni che utilizzano Modifier.Node
Quando crei modificatori personalizzati con Modifier.Node, ecco alcune situazioni comuni che potresti incontrare.
Zero parametri
Se il modificatore non ha parametri, non deve mai essere aggiornato e, inoltre, non deve essere una classe di dati. Di seguito è riportata un'implementazione di esempio di un modificatore che applica una quantità fissa di padding a 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) } } }
Fare riferimento alle composizioni locali
I modificatori Modifier.Node non osservano automaticamente le modifiche agli oggetti di stato di Compose, come CompositionLocal. Il vantaggio dei modificatori Modifier.Node rispetto ai modificatori creati con una factory composable è che possono leggere il valore della composizione locale dalla posizione in cui il modificatore viene utilizzato nell'albero dell'UI, non dalla posizione in cui il modificatore viene allocato, utilizzando currentValueOf.
Tuttavia, le istanze dei nodi modificatori non osservano automaticamente le modifiche dello stato. Per reagire automaticamente a una modifica della composizione locale, puoi leggere il suo valore corrente all'interno di un ambito:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
Questo esempio osserva il valore di LocalContentColor per disegnare uno sfondo in base al suo colore. Poiché ContentDrawScope osserva le modifiche dello snapshot, viene ridisegnato automaticamente quando il valore di LocalContentColor cambia:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Per reagire alle modifiche dello stato al di fuori di un ambito e aggiornare automaticamente il tuo
modificatore, utilizza un ObserverModifierNode.
Ad esempio, Modifier.scrollable utilizza questa tecnica per
osservare le modifiche in LocalDensity. Nell'esempio seguente viene mostrato un esempio semplificato:
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) } }
Animare un modificatore
Le implementazioni di Modifier.Node hanno accesso a un coroutineScope. Ciò consente
di utilizzare le API Animatable di Compose. Ad esempio, questo snippet modifica il CircleNode mostrato in precedenza per eseguire ripetutamente la dissolvenza in entrata e in uscita:
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) ) { } } } }
Condividere lo stato tra i modificatori utilizzando la delega
I modificatori Modifier.Node possono delegare ad altri nodi. Esistono molti casi d'uso per questa funzionalità, ad esempio l'estrazione di implementazioni comuni tra diversi modificatori, ma può essere utilizzata anche per condividere lo stato comune tra i modificatori.
Ad esempio, un'implementazione di base di un nodo modificatore cliccabile che condivide i dati di interazione:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Disattivare l'invalidazione automatica dei nodi
I nodi Modifier.Node vengono invalidati automaticamente quando il relativo ModifierNodeElement chiama l'aggiornamento. Per i modificatori complessi, potresti voler disattivare questo comportamento per ottenere un controllo più granulare su quando il modificatore invalida le fasi.
Questa funzionalità è particolarmente utile se il modificatore personalizzato modifica sia il layout sia il disegno. La disattivazione dell'invalidazione automatica consente di invalidare solo il disegno quando cambiano solo le proprietà correlate al disegno, come color. In questo modo si evita di invalidare il layout e si può migliorare il rendimento del modificatore.
Nell'esempio seguente viene mostrato un esempio ipotetico di un modificatore con una lambda color, size e onClick come proprietà. Questo modificatore invalida solo ciò che è necessario, saltando qualsiasi invalidazione non necessaria:
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) } } }