Compose fornisce molti modificatori per i comportamenti comuni pronti all'uso, ma puoi anche creare modificatori personalizzati.
I modificatori sono composti da più parti:
- Una fabbrica di modificatori
- Si tratta di una funzione di estensione su
Modifier
, che fornisce un'API idiomatica per il modificatore e consente di concatenare facilmente i modificatori. La fabbrica di modificatori produce gli elementi modificatori utilizzati da Compose per modificare la tua 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 delle
funzionalità necessarie. Spesso, il modo più semplice per implementare un modificatore personalizzato è
implementare una fabbrica di modificatori personalizzati che combini altre fabbriche 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 semplicemente utilizzando quelli esistenti. Ad esempio, Modifier.clip()
viene implementato utilizzando il modificatore
graphicsLayer
. Questa strategia utilizza gli elementi modificatori esistenti e tu
fornisci la tua fabbrica di modificatori personalizzati.
Prima di implementare un modificatore personalizzato, verifica se puoi utilizzare la stessa strategia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
In alternativa, se ti accorgi di ripetere spesso lo stesso gruppo di modificatori, puoi raggrupparli in un modificatore personalizzato:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Creare un modificatore personalizzato utilizzando una fabbrica di modificatori componibili
Puoi anche creare un modificatore personalizzato utilizzando una funzione componibile per passare valori a un modificatore esistente. Questo è noto come fabbrica di modificatori componibili.
L'utilizzo di una factory di modificatori componibili 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 dell'alpha 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
componibili:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Questo approccio presenta alcune avvertenze descritte di seguito.
I valori di CompositionLocal
vengono risolti nel sito di chiamata della fabbrica di modificatori
Quando crei un modificatore personalizzato utilizzando una fabbrica di modificatori componibili, le variabili locali della composizione prendono il valore dall'albero della composizione in cui vengono create, non utilizzate. Ciò può portare a risultati inaspettati. Ad esempio, prendi l'esempio di modificatore locale di composizione riportato sopra, implementato in modo leggermente diverso utilizzando una funzione componibile:
@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 un
Modifier.Node
personalizzato, poiché le impostazioni internazionali di composizione verranno
risolte correttamente nel sito di utilizzo e possono essere sollevate in modo sicuro.
I modificatori delle funzioni componibili non vengono mai ignorati
I modificatori di fabbrica componibili non vengono mai ignorati perché le funzioni componibili 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 avviene di frequente.
I modificatori di funzioni componibili devono essere chiamati all'interno di una funzione componibile
Come tutte le funzioni componibili, un modificatore di fabbrica componibile deve essere chiamato all'interno della composizione. Ciò limita la posizione in cui un modificatore può essere spostato, in quanto non può mai essere spostato fuori dalla composizione. Al contrario, le factory di modificatori non componibili possono essere estratte dalle funzioni componibili per consentire un riutilizzo più semplice e migliorare le prestazioni:
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 dei modificatori personalizzati 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ù performante 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.Node
che contiene la logica e lo stato del modificatore. - Un
ModifierNodeElement
che crea e aggiorna le istanze dei nodi modificatori. - Un generatore di modificatori facoltativo, come descritto sopra.
Le classi ModifierNodeElement
sono stateless e vengono allocate nuove istanze a ogni ricomposizione, mentre le classi Modifier.Node
possono essere stateful e 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 del 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 eseguire l'override del metodo draw.
I tipi disponibili sono i seguenti:
Nodo |
Utilizzo |
Link di esempio |
Un |
||
Un |
||
L'implementazione di questa interfaccia consente a |
||
Un |
||
Un |
||
Un |
||
Un |
||
Un |
||
Gli |
||
Un Ciò 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 dell'ora sull'elemento, il nodo attiva un nuovo disegno e il suo colore viene aggiornato correttamente. È
possibile disattivare l'annullamento automatico come descritto
di seguito.
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 eseguire l'override dei seguenti metodi:
create
: è la funzione che crea un'istanza del nodo modificatore. Questo viene chiamato per creare il nodo quando il modificatore viene applicato per la prima volta. In genere, questo comporta la costruzione del nodo e la sua configurazione con i parametri che sono stati passati alla fabbrica di modificatori.update
: questa funzione viene chiamata ogni volta che questo modificatore viene fornito nello stesso punto in cui esiste già questo nodo, ma una proprietà è cambiata. Ciò è determinato dal metodoequals
della 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 delle prestazioni offerti daModifier.Node
; pertanto, devi aggiornare il nodo esistente anziché crearne uno nuovo nel metodoupdate
. Nell'esempio del 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 il tuo 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 spaziatura interna.
Modifier factory
Questa è la superficie API pubblica del modificatore. La maggior parte delle implementazioni crea semplicemente 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. Ecco un esempio di implementazione di un modificatore che applica una quantità fissa di spaziatura interna a un elemento componibile:
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) } } }
Riferimento ai locali di composizione
I modificatori Modifier.Node
non rilevano automaticamente le modifiche agli oggetti di stato di Compose, come CompositionLocal
. Il vantaggio dei modificatori Modifier.Node
rispetto a quelli creati con una factory componibile è che possono leggere il valore della composizione locale dal punto in cui il modificatore viene utilizzato nell'albero dell'interfaccia utente, non dal punto in cui viene allocato, utilizzando currentValueOf
.
Tuttavia, le istanze dei nodi modificatori non osservano automaticamente le modifiche dello stato. Per reagire automaticamente a una modifica locale della composizione, puoi leggere il suo valore corrente all'interno di un ambito:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
&IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
Questo esempio osserva il valore di LocalContentColor
per disegnare uno sfondo
in base al suo colore. Poiché ContentDrawScope
osserva le modifiche agli snapshot, questo
viene ridisegnato automaticamente quando cambia il valore di LocalContentColor
:
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 modificatore, utilizza un ObserverModifierNode
.
Ad esempio, Modifier.scrollable
utilizza questa tecnica per
osservare le variazioni di LocalDensity
. Di seguito è riportato 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) } }
Modificatore di animazione
Le implementazioni di Modifier.Node
hanno accesso a un coroutineScope
. Ciò consente
l'utilizzo delle API Compose Animatable. Ad esempio, questo snippet modifica
CircleNode
riportato sopra in modo che appaia e scompaia ripetutamente:
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) ) { } } } }
Stato di condivisione 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 in diversi modificatori,
ma può essere utilizzata anche per condividere uno stato comune tra i modificatori.
Ad esempio, un'implementazione di base di un nodo modificatore su cui è possibile fare clic che condivide i dati di interazione:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Disattivazione dell'invalidazione automatica dei nodi
I nodi Modifier.Node
vengono invalidati automaticamente quando vengono aggiornate le chiamate ModifierNodeElement
corrispondenti. A volte, in un modificatore più complesso, potresti
voler disattivare questo comportamento per avere un controllo più granulare su quando
il modificatore invalida le fasi.
Ciò può essere particolarmente utile se il modificatore personalizzato modifica sia il layout sia
il disegno. Se disattivi l'invalidazione automatica, puoi invalidare il disegno solo quando cambiano le proprietà correlate al disegno, come color
, e non invalidare il layout.
In questo modo, puoi migliorare il rendimento del modificatore.
Di seguito è riportato un esempio ipotetico con un modificatore che ha le lambda color
, size
e onClick
come proprietà. Questo modificatore invalida solo ciò che è obbligatorio e ignora qualsiasi invalidazione che non lo è:
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) } } }