Crea modificatori personalizzati

Compose fornisce molti modificatori per comportamenti comuni fin da subito, ma puoi anche creare i tuoi 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 concatenare facilmente i modificatori. La fabbrica del modificatore produce gli elementi di modifica utilizzati da Compose per modificare la UI.
  • Un elemento di modifica
    • Qui puoi implementare il comportamento del modificatore.

Esistono diversi modi per implementare un modificatore personalizzato, a seconda della funzionalità richiesta. Spesso, il modo più semplice per implementare un modificatore personalizzato è solo implementare una fabbrica di modificatori personalizzati che combina altri produttori di modificatori già definiti. Se hai bisogno di un comportamento più personalizzato, implementa l'elemento modificato utilizzando le API Modifier.Node, che sono di livello inferiore ma offrono una maggiore flessibilità.

Concatena i modificatori esistenti

Spesso è possibile creare modificatori personalizzati usando quelli esistenti. Ad esempio, Modifier.clip() viene implementato utilizzando il modificatore graphicsLayer. Questa strategia utilizza elementi di modifica esistenti e tu fornisci una fabbrica personalizzata di modificatori.

Prima di implementare il tuo modificatore personalizzato, verifica se puoi usare 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 aggregarli all'interno del 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 API di animazione supportate dallo stato Compose. Ad esempio, il seguente snippet mostra un modificatore che anima una modifica 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 fabbrica di modificatori componibili:

@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 si crea un modificatore personalizzato utilizzando una fabbrica di modificatori componibili, gli abitanti locali della composizione prendono il valore dall'albero delle composizioni in cui vengono creati, non vengono utilizzati. Ciò può portare a risultati imprevisti. Ad esempio, prendiamo l'esempio del 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 ti aspettavi il funzionamento del modificatore, utilizza un elemento Modifier.Node personalizzato, poiché la composizione locale verrà risolta correttamente sul sito di utilizzo e potrà essere sollevata in sicurezza.

I modificatori di funzione componibili non vengono mai ignorati

I modificatori di fabbrica componibili non vengono mai saltati perché le funzioni componibili che hanno valori restituiti non possono essere ignorate. Ciò significa che la funzione di modifica verrà richiamata 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, un modificatore di fabbrica componibile deve essere chiamato dalla composizione. Questo limita i punti in cui un modificatore può essere sollevato, dato che non può mai essere sollevato fuori dalla composizione. In confronto, le fabbriche di modificatori non componibili possono essere sollevate dalle funzioni componibili 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
}

Implementa 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ù efficace per creare modificatori personalizzati.

Implementa un modificatore personalizzato utilizzando Modifier.Node

L'implementazione di un modificatore personalizzato utilizzando Modifier.Node prevede tre fasi:

  • Un'implementazione di Modifier.Node che contiene la logica e lo stato del modificatore.
  • Un elemento ModifierNodeElement che crea e aggiorna le istanze dei nodi di modifica.
  • Una fabbrica facoltativa di modificatori, come descritto sopra.

Le classi ModifierNodeElement sono stateless e a ogni ricomposizione viene allocata nuove istanze, mentre le classi Modifier.Node possono essere stateful, possono sopravvivere in più ricomposizioni e possono anche essere riutilizzate.

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 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 alla funzione di modifica.

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 consente di eseguire l'override del metodo di disegno.

I tipi disponibili sono i seguenti:

Nodo

Utilizzo

Link di esempio

LayoutModifierNode

Un Modifier.Node che cambia il modo in cui vengono misurati e disposti i contenuti aggregati.

Esempio

DrawModifierNode

Un Modifier.Node che disegna nello spazio del layout.

Esempio

CompositionLocalConsumerModifierNode

L'implementazione di questa interfaccia consente al tuo Modifier.Node di leggere le informazioni locali sulle composizioni.

Esempio

SemanticsModifierNode

Una Modifier.Node che aggiunge una chiave/valore semantica da utilizzare in test, accessibilità e casi d'uso simili.

Esempio

PointerInputModifierNode

Un elemento Modifier.Node che riceve PointerInputChanges.

Esempio

ParentDataModifierNode

Un elemento Modifier.Node che fornisce dati al layout principale.

Esempio

LayoutAwareModifierNode

Un Modifier.Node che riceve callback onMeasured e onPlaced.

Esempio

GlobalPositionAwareModifierNode

Un elemento Modifier.Node che riceve un callback onGloballyPositioned con l'ultimo LayoutCoordinates del layout quando la posizione globale dei contenuti potrebbe essere cambiata.

Esempio

ObserverModifierNode

I Modifier.Node che implementano ObserverNode possono fornire la propria implementazione di onObservedReadsChanged che verrà chiamato in risposta alle modifiche agli oggetti snapshot letti all'interno di un blocco observeReads.

Esempio

DelegatingNode

Un Modifier.Node che è in grado di delegare il lavoro ad altre istanze di Modifier.Node.

Questo può essere utile per comporre più implementazioni di nodi in una sola.

Esempio

TraversableNode

Consente alle classi Modifier.Node di spostarsi verso l'alto o verso il basso nell'albero dei nodi per classi dello stesso tipo o per una chiave specifica.

Esempio

I nodi vengono invalidati automaticamente quando viene chiamato l'aggiornamento nell'elemento corrispondente. Poiché il nostro esempio è un DrawModifierNode, ogni volta che viene richiesto un aggiornamento dell'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 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:

  1. create: questa è la funzione che crea un'istanza del nodo modificatore. Questo viene chiamato per creare il nodo quando viene applicato per la prima volta il modificatore. In genere, ciò deve creare il nodo e configurarlo con i parametri che sono stati trasmessi alla fabbrica del modificatore.
  2. update: questa funzione viene richiamata ogni volta che questo modificatore viene fornito nello stesso punto in cui questo nodo esiste già, ma è stata modificata una proprietà. Questo è determinato dal metodo equals della classe. Il nodo modificatore creato in precedenza viene inviato come parametro alla chiamata update. 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 migliorare le prestazioni grazie a Modifier.Node. Di conseguenza, devi aggiornare il nodo esistente anziché crearne uno nuovo nel metodo update. Nell'esempio della nostra cerchia, il colore del nodo viene aggiornato.

Inoltre, le implementazioni ModifierNodeElement devono implementare anche equals e hashCode. update viene chiamato solo se un confronto di uguale 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 alla necessità di aggiornare un nodo o se vuoi evitare le classi di dati 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 semplicemente 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 per 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, ecco alcune situazioni comuni che potresti riscontrare.

Zero parametri

Se il modificatore non ha parametri, non deve mai essere aggiornato e, Inoltre, non deve essere una classe di dati. Di seguito è riportato un esempio di implementazione 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 le modifiche agli oggetti di stato Componi, come CompositionLocal. Il vantaggio che i modificatori di Modifier.Node hanno rispetto ai modificatori appena creati con una fabbrica componibile è che possono leggere il valore della composizione locale nel punto in cui il modificatore viene utilizzato nella struttura dell'interfaccia utente, non dove è allocato il modificatore, utilizzando currentValueOf.

Tuttavia, le istanze del nodo modificatore non osservano automaticamente i cambiamenti di stato. Per reagire automaticamente a una modifica locale della composizione, puoi leggerne il valore attuale all'interno di un ambito:

Questo esempio osserva il valore dell'elemento LocalContentColor per disegnare uno sfondo in base al colore. Poiché ContentDrawScope osserva le modifiche dello snapshot, questo ricrea 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 ai cambiamenti di stato che non rientrano in un ambito e aggiornare automaticamente il modificatore, utilizza una ObserverModifierNode.

Ad esempio, Modifier.scrollable utilizza questa tecnica per osservare le modifiche 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 il CircleNode sopra per la dissolvenza 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. Ci sono molti casi d'uso per questo scopo, come l'estrazione di implementazioni comuni tra diversi modificatori, ma può essere utilizzato anche per condividere lo stato comune tra i modificatori.

Ad esempio, un'implementazione di base di un nodo di modifica cliccabile che condivide i dati delle interazioni:

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 le chiamate ModifierNodeElement corrispondenti vengono aggiornate. 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.

Questo può essere particolarmente utile se il modificatore personalizzato modifica sia il layout sia il disegno. Se disattivi l'invalidazione automatica, puoi semplicemente invalidare il disegno quando solo le proprietà correlate al disegno, come color, la modifica e non il layout. In questo modo puoi migliorare il rendimento del modificatore.

Di seguito è mostrato un esempio ipotetico con un modificatore che ha le proprietà lambda color, size e onClick. Questo modificatore invalida solo le informazioni obbligatorie e salta qualsiasi annullamento 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)
        }
    }
}