Compose fornisce subito molti modificatori per i comportamenti comuni, ma puoi anche creare modificatori personalizzati.
I modificatori sono costituiti da più parti:
- Una fabbrica di modificatori
- Questa è una funzione di estensione su
Modifier
, che fornisce un'API idiomatica per il modificatore e consente di concatenarli facilmente. La Modificatore di fabbrica produce gli elementi di modifica utilizzati da Compose per modificare la tua UI.
- Questa è una funzione di estensione su
- Un elemento di modifica
- Qui puoi implementare il comportamento del modificatore.
Esistono diversi modi per implementare un modificatore personalizzato a seconda
funzionalità necessarie. Spesso, il modo più semplice per implementare un modificatore personalizzato
solo per implementare una fabbrica di modificatori personalizzati che combina altri parametri
insieme di modificatori. Se hai bisogno di un comportamento più personalizzato, implementa il
di modifica utilizzando le API Modifier.Node
, che sono di livello inferiore
offrono una maggiore flessibilità.
Concatena i modificatori esistenti
Spesso è possibile creare modificatori personalizzati utilizzando
modificatori. Ad esempio, Modifier.clip()
viene implementato utilizzando la proprietà
Modificatore graphicsLayer
. Questa strategia utilizza elementi di modifica esistenti e tu
per creare il tuo
fabbrica di modificatori personalizzati.
Prima di implementare il tuo modificatore personalizzato, verifica se puoi utilizzare lo stesso strategia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Oppure, se ti accorgi di ripetere spesso lo stesso gruppo di modificatori, puoi aggregale con il tuo modificatore:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Crea 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. In questo caso, è nota come fabbrica di modificatori componibili.
L'utilizzo di una fabbrica di modificatori componibili per creare un modificatore consente anche di utilizzare
API di scrittura di livello superiore, come animate*AsState
e altre Compose
API di animazione supportate dallo stato. Ad esempio, il seguente snippet mostra un
modificatore che anima una modifica alpha se 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 implementare questa funzionalità è utilizzare un modello
fabbrica modificatore:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Questo approccio prevede alcune avvertenze descritte di seguito.
I valori CompositionLocal
vengono risolti sul sito di chiamata della fabbrica del modificatore
Quando crei un modificatore personalizzato utilizzando una fabbrica di modificatori componibili, I residenti trattengono il valore dall'albero delle composizioni in cui sono creati, non in uso. Ciò può portare a risultati imprevisti. Ad esempio, considera la composizione di modifica locale precedente, implementato in modo leggermente diverso utilizzando 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 ti aspettavi il funzionamento del modificatore, utilizza una
Modifier.Node
, invece, poiché la composizione locale sarà
correttamente risolti sul sito di utilizzo e sollevabili in sicurezza.
I modificatori di funzione componibili non vengono mai ignorati
I modificatori di fabbrica componibili non vengono mai saltati perché le funzioni componibili con valori restituiti non possono essere ignorati. Ciò significa che la funzione di modifica verrà chiamato a ogni ricomposizione, il che potrebbe essere costoso se si ricompone spesso.
I modificatori di funzione componibile devono essere chiamati all'interno di una funzione componibile
Come tutte le funzioni componibili, è necessario richiamare un modificatore di fabbrica componibile all'interno della composizione. Questo limita le aree a cui è possibile issare un modificatore, non verranno mai sollevate dalla composizione. In confronto, il modificatore non componibile possono essere sollevate dalle funzioni componibili per facilitarne il riutilizzo 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 }
Implementa il comportamento del modificatore personalizzato utilizzando Modifier.Node
Modifier.Node
è un'API di livello inferiore per la creazione di modificatori in Compose. it
è la stessa API in cui Compose implementa i propri modificatori ed è la
per creare modificatori personalizzati.
Implementa un modificatore personalizzato utilizzando Modifier.Node
L'implementazione di un modificatore personalizzato utilizzando Modifier.Node prevede tre passaggi:
- Un'implementazione di
Modifier.Node
che contenga la logica e lo stato del modificatore. - Una
ModifierNodeElement
che crea e aggiorna il modificatore di Compute Engine. - Una fabbrica facoltativa di modificatori, come descritto sopra.
ModifierNodeElement
classi sono stateless e sono allocate nuove istanze ciascuna
ricomposizione, mentre le classi Modifier.Node
possono essere stateful e sopravvivere
in più ricomposizioni e possono anche essere riutilizzati.
La sezione seguente descrive ogni parte e mostra un esempio di costruzione di un modificatore personalizzato per disegnare un cerchio.
Modifier.Node
L'implementazione Modifier.Node
(in questo esempio, CircleNode
) implementa
il
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 un cerchio con il colore passato al modificatore personalizzata.
Un nodo implementa Modifier.Node
e zero o più tipi di nodi. Esistono
diversi tipi di nodi in base alla funzionalità richiesta dal modificatore. La
nell'esempio precedente deve essere in grado di disegnare, quindi implementa DrawModifierNode
, che
consente di ignorare il metodo di disegno.
I tipi disponibili sono i seguenti:
Nodo |
Utilizzo |
Link di esempio |
Un |
||
Un |
||
L'implementazione di questa interfaccia consente al tuo |
||
Una |
||
Un elemento |
||
Un elemento |
||
Un |
||
Un elemento |
||
I |
||
Un Questo può essere utile per comporre più implementazioni di nodi in una sola. |
||
Consente alle classi |
I nodi vengono invalidati automaticamente quando viene chiamato l'aggiornamento sui server corrispondenti
. Poiché il nostro esempio è DrawModifierNode
, viene richiesto qualsiasi aggiornamento
all'elemento, il nodo attiva un nuovo disegno e il suo colore si aggiorna correttamente. È
è possibile disattivare l'invalidazione automatica, come descritto di seguito.
ModifierNodeElement
Una ModifierNodeElement
è una classe immutabile che contiene i dati da creare o
aggiorna 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. Questo diventa per creare il nodo quando viene applicato per la prima volta il modificatore. Di solito, questo equivale a costruire il nodo e configurarlo con i parametri sono stati trasmessi alla fabbrica di modificatori.update
: questa funzione viene richiamata ogni volta che questo modificatore viene fornito nella stesso punto in cui esiste già questo nodo, ma è stata modificata una proprietà. Questo è determinato dal metodoequals
della classe. Il nodo del modificatore creato in precedenza viene inviato come parametro alla chiamataupdate
. A questo punto, dovresti aggiornare i nodi che corrispondano alle proprietà aggiornate parametri. La possibilità di riutilizzare in questo modo i nodi è fondamentale il miglioramento del rendimento ottenuto daModifier.Node
; di conseguenza, devi aggiornare esistente anziché crearne uno nuovo nel metodoupdate
. Nel nostro esempio di un cerchio, il colore del nodo viene aggiornato.
Inoltre, le implementazioni ModifierNodeElement
devono implementare anche equals
e hashCode
. update
verrà chiamato solo se un confronto è uguale a
l'elemento precedente restituisce false.
L'esempio precedente utilizza una classe di dati per ottenere questo risultato. Questi metodi consentono
verifica se un nodo deve essere aggiornato o meno. Se l'elemento ha proprietà che
non contribuiscono alla necessità di aggiornare un nodo o alla necessità
per motivi di compatibilità binaria, puoi implementare manualmente equals
e hashCode
, ad esempio l'elemento di modifica della spaziatura interna.
Fabbrica del modificatore
Questa è la superficie API pubblica del tuo modificatore. La maggior parte delle implementazioni crea l'elemento di modifica e aggiungilo alla catena di modificatori:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Esempio completo
Queste tre parti si uniscono per creare il modificatore personalizzato che consente di disegnare un cerchio
utilizzando le API di 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 con Modifier.Node
Quando crei modificatori personalizzati con Modifier.Node
, di seguito sono riportate alcune situazioni comuni che
durante l'incontro.
Zero parametri
Se il modificatore non ha parametri, non deve mai essere aggiornato Inoltre, non deve essere necessariamente una classe di dati. Ecco un'implementazione di esempio di un modificatore che applica una quantità fissa di spaziatura interna a un 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) } } }
Fare riferimento alla composizione locale
I modificatori Modifier.Node
non osservano automaticamente i cambiamenti nello stato Scrivi
come CompositionLocal
. Il vantaggio che i modificatori di Modifier.Node
hanno su
creati con una fabbrica componibile è che possono leggere
il valore della composizione locale in cui viene utilizzato il modificatore nell'interfaccia utente
non dove è allocato il modificatore, utilizzando currentValueOf
.
Tuttavia, le istanze del nodo modificatore non osservano automaticamente i cambiamenti di stato. A a una composizione locale, puoi leggerne la descrizione all'interno di un ambito:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
eIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
Questo esempio osserva il valore di LocalContentColor
per disegnare uno sfondo basato
sul suo colore. Poiché ContentDrawScope
osserva le modifiche dello snapshot,
ridisegna 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 a modifiche di stato che non rientrano in un ambito e aggiornare automaticamente
utilizza un ObserverModifierNode
.
Ad esempio, Modifier.scrollable
utilizza questa tecnica per
osserva i cambiamenti in 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 animazione
Le implementazioni Modifier.Node
hanno accesso a un coroutineScope
. Ciò consente
l'utilizzo delle API Compose Animatable. Ad esempio, questo snippet modifica
CircleNode
dall'alto per dissolversi in entrata e in uscita ripetutamente:
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) ) { } } } }
Condivisione dello stato tra modificatori utilizzando la delega
I modificatori Modifier.Node
possono delegare ad altri nodi. Esistono molti casi d'uso
per questo, ad esempio estrarre implementazioni comuni con diversi modificatori,
ma può essere utilizzato anche per condividere lo stato comune dei modificatori.
Ad esempio, un'implementazione di base di un nodo di modifica cliccabile che dati sull'interazione:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Disattivazione dell'annullamento automatico dei nodi
I nodi Modifier.Node
vengono annullati automaticamente quando
Aggiornamento di ModifierNodeElement
chiamate. A volte, in un modificatore più complesso,
disattivare questo comportamento per avere un controllo più capillare su quando
il modificatore invalida le fasi.
Ciò può essere particolarmente utile se il modificatore personalizzato modifica sia il layout che
disegnare. Se disattivi l'invalidazione automatica, puoi semplicemente invalidare il disegno quando
solo le proprietà correlate al disegno, come color
, modifica e non invalida il layout.
In questo modo puoi migliorare il rendimento del modificatore.
Di seguito è mostrato un esempio ipotetico con un modificatore che ha color
,
size
e onClick
lambda come proprietà. Questo modificatore invalida solo le informazioni
obbligatorio e salta qualsiasi annullamento convalida che non sia:
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) } } }