Crea modificatori personalizzati

Compose offre fin da subito molti modificatori per i comportamenti comuni, ma puoi anche creare modificatori personalizzati.

I modificatori sono composti da più parti:

  • Impostazione predefinita di un modificatore
    • Questa è una funzione di estensione su Modifier, che fornisce un'API idiomatica per il modificatore e consente di concatenare facilmente i modificatori. Il valore di fabbrica del modificatore produce gli elementi di modifica 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 delle funzionalità necessarie. Spesso, il modo più semplice per implementare un modificatore personalizzato è semplicemente implementare un modificatore personalizzato che combina altre fabbriche di modificatori già definite. Se hai bisogno di un comportamento più personalizzato, implementa l'elemento di modifica usando le API Modifier.Node, che sono di livello inferiore, ma offrono una maggiore flessibilità.

Concatenamento dei modificatori esistenti

Spesso è possibile creare modificatori personalizzati semplicemente utilizzando i modificatori esistenti. Ad esempio, Modifier.clip() viene implementato utilizzando il modificatore graphicsLayer. che prevede l'uso di elementi modificatori esistenti, mentre tu fornisci i tuoi 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 includerli nel tuo modificatore:

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

Crea un modificatore personalizzato usando il valore di fabbrica di un modificatore componibile

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

L'utilizzo di un modificatore componibile per creare un modificatore consente anche di utilizzare API di composizione 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 alpha quando viene attivata/disattivata:

@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 elemento CompositionLocal, il modo più semplice per implementarlo è utilizzare un modificatore componibile:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Questo approccio presenta alcune avvertenze dettagliate di seguito.

I valori CompositionLocal sono risolti sul sito di chiamata del fabbricante del modificatore

Quando crei un modificatore personalizzato utilizzando una fabbrica di modificatori componibili, gli elementi locali della composizione prendono il valore dall'albero delle composizioni in cui vengono creati, non utilizzati. Ciò può portare a risultati imprevisti. Ad esempio, prendiamo l'esempio del modificatore locale della composizione sopra riportato, 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 è come ti aspetti il funzionamento del modificatore, utilizza un elemento Modifier.Node personalizzato, perché i valori locali della composizione verranno risolti correttamente sul sito di utilizzo e possono essere sollevati in sicurezza.

I modificatori delle funzioni componibili non vengono mai ignorati

I modificatori di fabbrica componibili non vengono mai saltati perché le funzioni componibili con valori restituiti non possono essere ignorate. Ciò significa che la funzione di modifica verrà chiamata a ogni ricomposizione, il che potrebbe essere costoso se si ricompone frequentemente.

I modificatori di funzione componibile devono essere richiamati all'interno di una funzione componibile

Come tutte le funzioni componibili, un modificatore di fabbrica componibile deve essere chiamato dall'interno di una composizione. Questo limita gli spazi in cui è possibile issare un modificatore, 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 facilitarne il riutilizzo 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 mediante Modifier.Node prevede tre parti:

  • Un'implementazione di Modifier.Node contenente la logica e lo stato del modificatore.
  • Una risorsa ModifierNodeElement che crea e aggiorna le istanze di nodo modificatori.
  • Un modificatore facoltativo di fabbrica, come descritto sopra.

Le classi ModifierNodeElement sono stateless e a nuove istanze vengono assegnate ogni ricomposizione, mentre le classi Modifier.Node possono essere stateful, sopravvivere a diverse ricomposizioni e persino riutilizzarle.

La seguente sezione descrive ogni parte e mostra un esempio di creazione di un modificatore personalizzato per tracciare 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, viene disegnato il cerchio con il colore trasmesso 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 riportato sopra deve essere in grado di disegnare, quindi implementa DrawModifierNode, il 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 elemento Modifier.Node che attira lo spazio del layout.

Esempio

CompositionLocalConsumerModifierNode

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

Esempio

SemanticsModifierNode

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

Esempio

PointerInputModifierNode

Un Modifier.Node che riceve PointerInputChanges.

Esempio

ParentDataModifierNode

Un elemento 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 l'ultimo LayoutCoordinates del layout quando la posizione globale dei contenuti potrebbe essere cambiata.

Esempio

ObserverModifierNode

Gli elementi 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 che è in grado di delegare il lavoro ad altre istanze 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 e verso il basso nella struttura ad albero dei nodi per classi dello stesso tipo o per una chiave particolare.

Esempio

I nodi vengono invalidati automaticamente quando viene chiamato l'aggiornamento dell'elemento corrispondente. Dal momento che il nostro esempio è un DrawModifierNode, ogni volta che viene richiamato l'aggiornamento dell'elemento, il nodo attiva un nuovo disegno e il colore si aggiorna correttamente. È possibile disattivare l'annullamento automatico come descritto di seguito.

ModifierNodeElement

ModifierNodeElement è una classe immutabile contenente i dati per la creazione o l'aggiornamento del 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 ModifierNodeElement devono sostituire i seguenti metodi:

  1. 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 equivale alla creazione del nodo e alla sua configurazione con i parametri trasmessi all'impostazione di fabbrica 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 è stata modificata una proprietà. Ciò viene 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 capacità dei nodi di essere riutilizzati in questo modo è fondamentale per i miglioramenti delle prestazioni apportati da Modifier.Node; pertanto, devi aggiornare il nodo esistente invece di crearne uno nuovo nel metodo update. Nel nostro esempio di cerchio, il colore del nodo viene aggiornato.

Inoltre, le implementazioni ModifierNodeElement devono implementare equals e hashCode. update verrà chiamato solo se un confronto di tipo "uguale" con l'elemento precedente restituisce false.

L'esempio riportato sopra utilizza una classe di dati per raggiungere questo obiettivo. 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 vuoi evitare le classi di dati per motivi di compatibilità binaria, puoi implementare manualmente equals e hashCode, ad esempio l'elemento modificatore di spaziatura.

Fabbrica dei modificatori

Questa è la superficie API pubblica del 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 vengono combinate 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 con l'utilizzo di 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 necessariamente una classe di dati. Ecco 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)
        }
    }
}

Riferimento locale alle composizioni

I modificatori Modifier.Node non osservano automaticamente le modifiche agli oggetti di stato Compose, come CompositionLocal. Il vantaggio che i modificatori Modifier.Node hanno rispetto ai modificatori appena creati con una fabbrica componibile è che possono leggere il valore della composizione locale da dove viene usato il modificatore nella struttura dell'interfaccia utente, non da dove è allocato, utilizzando currentValueOf.

Tuttavia, le istanze dei nodi di modifica non osservano automaticamente i cambiamenti di stato. Per reagire automaticamente a una modifica locale della composizione, puoi leggere il suo valore attuale 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 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 ai cambiamenti di stato al di fuori di un ambito e aggiornare automaticamente il modificatore, utilizza 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 di animazione

Le implementazioni Modifier.Node hanno accesso a un coroutineScope. Ciò consente di utilizzare le API Compose Animatable. Ad esempio, questo snippet modifica il valore CircleNode sopra indicato in modo da applicare 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 che utilizzano la delega

I modificatori Modifier.Node possono delegare ad altri nodi. Questo approccio può essere utilizzato in molti casi d'uso, ad esempio l'estrazione di implementazioni comuni in modificatori diversi, 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 sulle interazioni:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Disattivazione dell'annullamento automatico dell'annullamento dei nodi in corso...

I nodi Modifier.Node vengono invalidati automaticamente quando vengono aggiornate le chiamate ModifierNodeElement corrispondenti. A volte, con 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. La disattivazione dell'annullamento automatico dell'annullamento ti consente semplicemente di invalidare il disegno quando solo le proprietà correlate al disegno, come color, modificano e non invalida il layout. In questo modo puoi migliorare il rendimento del modificatore.

Di seguito è riportato un esempio ipotetico di ciò con un modificatore con proprietà lambda color, size e onClick. Questo modificatore rende non valido solo ciò che è richiesto e ignora qualsiasi annullamento che non è:

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