Crea modificatori personalizzati

Compose fornisce molti modificatori per i comportamenti comuni, ma puoi anche creare i tuoi modificatori personalizzati.

I modificatori sono composti da 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 del modificatore produce gli elementi del modificatore utilizzati da Compose per modificare la UI.
  • 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 personalizzati 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à.

Concatena i modificatori esistenti

Spesso è possibile creare modificatori personalizzati utilizzando quelli esistenti. Ad esempio, Modifier.clip() viene implementato utilizzando il modificatore graphicsLayer. Questa strategia utilizza elementi modificatori esistenti e tu fornisci la tua factory di modificatori personalizzati.

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 ti accorgi di ripetere spesso lo stesso gruppo di modificatori, puoi racchiuderli nel tuo modificatore:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Crea un modificatore personalizzato utilizzando una factory di modificatori componibili

Puoi anche creare un modificatore personalizzato utilizzando una funzione componibile per passare i valori a un modificatore esistente. Questo è noto come factory di modificatori componibili.

L'utilizzo di una factory di modificatori componibili per creare un modificatore ti consente anche di utilizzare API Compose di livello superiore, come animate*AsState e altre API di animazione supportate dallo stato di Compose. Ad esempio, il seguente snippet mostra un modificatore che anima una modifica alfa quando viene 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 alcuni aspetti da considerare, descritti in dettaglio nelle sezioni seguenti.

I valori CompositionLocal vengono risolti nel sito di chiamata della factory del modificatore

Quando crei un modificatore personalizzato utilizzando una factory di modificatori componibili, i valori locali della composizione prendono il valore dall'albero della composizione in cui vengono creati, non utilizzati. 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 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 è questo il modo in cui ti aspetti che funzioni il modificatore, utilizza un Modifier.Node personalizzato, poiché i local di composizione verranno risolti correttamente nel sito di utilizzo e possono essere sottoposti a hoisting in sicurezza.

I modificatori delle funzioni componibili non vengono mai ignorati

I modificatori di factory componibili non vengono mai saltati perché le funzioni componibili che hanno valori restituiti non possono essere saltate. Ciò significa che la funzione di modifica verrà chiamata a ogni ricomposizione, il che potrebbe essere costoso se la ricomposizione avviene di frequente.

I modificatori delle funzioni componibili devono essere chiamati all'interno di una funzione componibile

Come tutte le funzioni componibili, un modificatore di factory componibile deve essere chiamato dall'interno della composizione. Questo limita la posizione in cui un modificatore può essere sottoposto a hoisting, in quanto non può mai essere sottoposto a hoisting fuori dalla composizione. In confronto, le factory di modificatori non componibili possono essere spostate all'esterno delle 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
}

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

Implementa un modificatore personalizzato utilizzando Modifier.Node

L'implementazione di un modificatore personalizzato utilizzando Modifier.Node prevede 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 factory di modificatori facoltativo, come descritto in precedenza.

Le classi ModifierNodeElement sono stateless e vengono allocate nuove istanze a ogni ricomposizione, mentre le classi Modifier.Node possono essere stateful e sopravvivere a più ricomposizioni, oltre a poter 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 di modifica.

Un nodo implementa Modifier.Node, nonché zero o più tipi di nodo. 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 modifica il modo in cui i contenuti racchiusi vengono misurati e disposti.

Esempio

DrawModifierNode

Un Modifier.Node che disegna nello spazio del layout.

Esempio

CompositionLocalConsumerModifierNode

L'implementazione di questa interfaccia consente a Modifier.Node di leggere i valori locali di composizione.

Esempio

SemanticsModifierNode

Un Modifier.Node che aggiunge una coppia chiave/valore di semantica da utilizzare nei test, nell'accessibilità e in casi d'uso simili.

Esempio

PointerInputModifierNode

Un Modifier.Node che riceve PointerInputChanges.

Esempio

ParentDataModifierNode

Un Modifier.Node che fornisce dati al layout principale.

Esempio

LayoutAwareModifierNode

Un Modifier.Node che riceve i callback onMeasured e onPlaced.

Esempio

GlobalPositionAwareModifierNode

Un Modifier.Node che riceve un callback onGloballyPositioned con il LayoutCoordinates finale 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à chiamata in risposta alle modifiche agli oggetti snapshot letti all'interno di un blocco observeReads.

Esempio

DelegatingNode

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

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

Esempio

TraversableNode

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

Esempio

I nodi vengono invalidati automaticamente quando viene chiamato l'aggiornamento sull'elemento corrispondente. Poiché il nostro esempio è un DrawModifierNode, ogni volta che viene chiamato update sull'elemento, il nodo attiva un ridisegno e il suo colore viene aggiornato correttamente. È possibile disattivare l'annullamento automatico della convalida, come descritto nella sezione Disattiva l'annullamento automatico della convalida 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 eseguire l'override dei seguenti metodi:

  1. create: questa è la funzione che crea un'istanza del nodo del modificatore. Questo metodo viene chiamato per creare il nodo quando il modificatore viene applicato per la prima volta. In genere, questo equivale a creare il nodo e configurarlo con i parametri passati alla factory del modificatore.
  2. 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. Questo è determinato dal metodo equals della classe. Il nodo del 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 i miglioramenti delle prestazioni offerti da Modifier.Node. Pertanto, devi aggiornare il nodo esistente anziché crearne uno nuovo nel metodo update. 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 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 spaziatura interna.

Modifier factory

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 uniscono 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

Ecco alcune situazioni comuni che potresti riscontrare durante la creazione di modificatori personalizzati con Modifier.Node.

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 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)
        }
    }
}

Elementi locali di composizione di riferimento

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 semplicemente con una factory componibile è che possono leggere il valore della composizione locale da dove viene utilizzato il modificatore nell'albero della UI, non da dove viene allocato il modificatore, utilizzando currentValueOf.

Tuttavia, le istanze dei nodi modificatori non osservano automaticamente le modifiche di stato. Per reagire automaticamente alla modifica di un CompositionLocal, puoi leggere il suo valore corrente all'interno di un ambito:

Questo esempio osserva il valore di LocalContentColor per disegnare uno sfondo in base al suo colore. Poiché ContentDrawScope osserva le modifiche degli snapshot, questo 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 di 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 modifiche in LocalDensity. Un esempio semplificato è mostrato nell'esempio seguente:

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)
    }
}

Anima un modificatore

Le implementazioni di Modifier.Node hanno accesso a un coroutineScope. In questo modo, puoi utilizzare le API Compose Animatable. Ad esempio, questo snippet modifica il CircleNode mostrato in precedenza per applicare la dissolvenza in entrata e in uscita 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)
            ) {
            }
        }
    }
}

Condividi 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ò anche essere utilizzata 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)
    )
}

Disattiva l'annullamento automatico della convalida dei nodi

I nodi Modifier.Node vengono invalidati automaticamente quando le chiamate ModifierNodeElement corrispondenti vengono aggiornate. Per i modificatori complessi, potresti voler disattivare questo comportamento per ottenere un controllo più granulare su quando il modificatore invalida le fasi.

Ciò è particolarmente utile se il modificatore personalizzato modifica sia il layout che il disegno. La disattivazione dell'annullamento automatico della convalida ti consente di annullare la convalida del disegno solo quando cambiano le proprietà relative al disegno, come color. In questo modo eviti di invalidare il layout e puoi migliorare le prestazioni del modificatore.

Un esempio ipotetico di questo è mostrato nell'esempio seguente con un modificatore che ha un lambda color, size e onClick come proprietà. Questo modificatore invalida solo ciò che è necessario, saltando qualsiasi annullamento della convalida non necessario:

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)
        }
    }
}