Benutzerdefinierte Modifikatoren erstellen

Compose bietet viele Modifikatoren für gängige Verhaltensweisen, Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.

Modifikatoren haben mehrere Teile:

  • Eine Modifikatorfabrik
    • Dies ist eine Erweiterungsfunktion für Modifier. Sie bietet eine idiomatische API für den Modifikator und ermöglicht die einfache Verkettung von Modifikatoren. Die Modifikator-Factory erzeugt die Modifikatorelemente, die von Compose zum Ändern der UI verwendet werden.
  • Ein Modifizierelement
    • Hier können Sie das Verhalten des Modifiers implementieren.

Je nach erforderlicher Funktion gibt es mehrere Möglichkeiten, einen benutzerdefinierten Modifikator zu implementieren. Oft ist es am einfachsten, einen benutzerdefinierten Modifikator zu implementieren, indem Sie eine benutzerdefinierte Modifikator-Fabrik implementieren, die andere bereits definierte Modifikator-Fabriken kombiniert. Wenn Sie ein benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifizierelement mithilfe der Modifier.Node APIs. Diese sind zwar niedriger, bieten aber mehr Flexibilität.

Vorhandene Modifikatoren verketten

Oft ist es möglich, benutzerdefinierte Modifikatoren nur mithilfe vorhandener Modifikatoren zu erstellen. Modifier.clip() wird beispielsweise mit dem graphicsLayer-Modifikator implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifikator-Factory bereit.

Bevor Sie einen eigenen benutzerdefinierten Modifikator implementieren, prüfen Sie, ob Sie dieselbe Strategie verwenden können.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Wenn Sie feststellen, dass Sie dieselbe Gruppe von Modifikatoren häufig wiederholen, können Sie sie in einen eigenen Modifikator einschließen:

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

Benutzerdefinierten Modifikator mithilfe einer Factory mit zusammensetzbaren Modifikatoren erstellen

Sie können auch einen benutzerdefinierten Modifikator mit einer kombinierbaren Funktion erstellen, um Werte an einen vorhandenen Modifikator zu übergeben. Dies wird als „composable modifier factory“ bezeichnet.

Wenn Sie einen Modifikator mit einer Compose Modifier Factory erstellen, können Sie auch Compose APIs höherer Ebene wie animate*AsState und andere Compose State-basierte Animation APIs verwenden. Das folgende Snippet zeigt beispielsweise einen Modifikator, der eine Alphaänderung bei Aktivierung/Deaktivierung animiert:

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

Wenn Ihr benutzerdefinierter Modifikator eine bequeme Methode zum Bereitstellen von Standardwerten aus einem CompositionLocal ist, lässt sich dies am einfachsten mit einer zusammensetzbaren Modifikator-Factory implementieren:

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

Dieser Ansatz hat einige Einschränkungen, die unten beschrieben werden.

CompositionLocal-Werte werden auf der Aufrufseite der Modifikatorfabrik aufgelöst

Wenn Sie einen benutzerdefinierten Modifikator mit einer composable modifier factory erstellen, übernehmen Zusammensetzungs-Locals den Wert aus dem Kompositionbaum, in dem sie erstellt, nicht verwendet werden. Das kann zu unerwarteten Ergebnissen führen. Nehmen wir beispielsweise das Beispiel für den lokalen Modifikator für die Komposition oben, das mit einer kombinierbaren Funktion etwas anders implementiert wurde:

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

Wenn der gewünschte Effekt nicht erzielt wird, verwenden Sie stattdessen einen benutzerdefinierten Modifier.Node, da lokale Variablen in der Verwendungsstelle korrekt aufgelöst und sicher hochgehängt werden können.

Modifikatoren für komponierbare Funktionen werden nie übersprungen

Zusammensetzbare Factory-Modifikatoren werden niemals übersprungen, da zusammensetzbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifikatorfunktion bei jeder Neuzusammensetzung aufgerufen wird, was teuer sein kann, wenn sie häufig neu zusammengesetzt wird.

Modifikatoren für komponierbare Funktionen müssen innerhalb einer komponierbaren Funktion aufgerufen werden

Wie alle kompositionsfähigen Funktionen muss ein kompositionsfähiger Fabrik-Modifikator innerhalb der Komposition aufgerufen werden. Dies schränkt ein, wohin ein Modifikator verschoben werden kann, da er nie aus der Komposition verschoben werden kann. Im Gegensatz dazu können nicht zusammensetzbare Modifikatorfabriken aus zusammensetzbaren Funktionen herausgezogen werden, um die Wiederverwendung zu vereinfachen und die Leistung zu verbessern:

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
}

Benutzerdefiniertes Modifikatorverhalten mit Modifier.Node implementieren

Modifier.Node ist eine untergeordnete API zum Erstellen von Modifikatoren in Compose. Dies ist dieselbe API, in der Compose seine eigenen Modifikatoren implementiert, und die leistungsstärkste Methode zum Erstellen benutzerdefinierter Modifikatoren.

Benutzerdefinierten Modifikator mit Modifier.Node implementieren

Die Implementierung eines benutzerdefinierten Modifikators mithilfe von Modifier.Node besteht aus drei Teilen:

  • Eine Modifier.Node-Implementierung, die die Logik und den Status des Modifiers enthält.
  • Einem ModifierNodeElement, das Knoteninstanzen mit Modifikator erstellt und aktualisiert.
  • Eine optionale Modifikator-Fabrik, wie oben beschrieben.

ModifierNodeElement-Klassen sind zustandslos und es werden bei jeder Neuzusammensetzung neue Instanzen zugewiesen. Modifier.Node-Klassen können zustandsorientiert sein, über mehrere Neuzusammensetzungen hinweg bestehen und sogar wiederverwendet werden.

Im folgenden Abschnitt werden die einzelnen Teile beschrieben und ein Beispiel für die Erstellung eines benutzerdefinierten Modifiers zum Zeichnen eines Kreises gezeigt.

Modifier.Node

Die Implementierung von Modifier.Node (in diesem Beispiel CircleNode) implementiert die Funktion Ihres benutzerdefinierten Modifiers.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

In diesem Beispiel wird der Kreis mit der Farbe gezeichnet, die an die Modifikatorfunktion übergeben wurde.

Ein Knoten implementiert Modifier.Node sowie null oder mehr Knotentypen. Je nach Funktion, die Ihr Modifikator erfordert, gibt es verschiedene Knotentypen. Das Beispiel oben muss zeichnen können. Daher wird DrawModifierNode implementiert, wodurch die draw-Methode überschrieben werden kann.

Folgende Typen sind verfügbar:

Knoten

Verwendung

Beispiellink

LayoutModifierNode

Ein Modifier.Node, mit dem sich ändert, wie der umgebrochene Inhalt gemessen und dargestellt wird.

Beispiel

DrawModifierNode

Ein Modifier.Node, das in den Layoutbereich eindringt.

Beispiel

CompositionLocalConsumerModifierNode

Wenn du diese Schnittstelle implementierst, kann deine Modifier.Node lokale Fonts lesen.

Beispiel

SemanticsModifierNode

Ein Modifier.Node, das ein semantisches Schlüssel/Wert-Paar für Tests, Barrierefreiheit und ähnliche Anwendungsfälle hinzufügt.

Beispiel

PointerInputModifierNode

Eine Modifier.Node, die PointerInputChanges empfängt.

Beispiel

ParentDataModifierNode

Ein Modifier.Node, das dem übergeordneten Layout Daten zur Verfügung stellt.

Beispiel

LayoutAwareModifierNode

Ein Modifier.Node, das onMeasured- und onPlaced-Callbacks empfängt.

Beispiel

GlobalPositionAwareModifierNode

Ein Modifier.Node, das einen onGloballyPositioned-Callback mit der endgültigen LayoutCoordinates des Layouts empfängt, wenn sich die globale Position der Inhalte möglicherweise geändert hat.

Beispiel

ObserverModifierNode

Modifier.Node-Objekte, die ObserverNode implementieren, können ihre eigene onObservedReadsChanged-Implementierung bereitstellen, die als Reaktion auf Änderungen an Snapshot-Objekten aufgerufen wird, die in einem observeReads-Block gelesen werden.

Beispiel

DelegatingNode

Eine Modifier.Node, die Aufgaben an andere Modifier.Node-Instanzen delegieren kann.

Das kann nützlich sein, um mehrere Knotenimplementierungen zu einer zusammenzuführen.

Beispiel

TraversableNode

Ermöglicht es Modifier.Node-Klassen, den Knotenbaum nach Klassen desselben Typs oder nach einem bestimmten Schlüssel nach oben oder unten zu durchsuchen.

Beispiel

Knoten werden automatisch ungültig, wenn für das entsprechende Element die Funktion „update“ aufgerufen wird. Da es sich bei unserem Beispiel um eine DrawModifierNode handelt, löst der Knoten bei jedem Aufruf einer Aktualisierung für das Element eine Neuzeichnung aus und seine Farbe wird korrekt aktualisiert. Sie können die automatische Entwertung wie unten beschrieben deaktivieren.

ModifierNodeElement

Eine ModifierNodeElement ist eine unveränderliche Klasse, die die Daten zum Erstellen oder Aktualisieren des benutzerdefinierten Modifiers enthält:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

ModifierNodeElement-Implementierungen müssen die folgenden Methoden überschreiben:

  1. create: Mit dieser Funktion wird der Modifier-Knoten instanziiert. Diese Funktion wird aufgerufen, um den Knoten zu erstellen, wenn der Modifikator zum ersten Mal angewendet wird. In der Regel bedeutet das, den Knoten zu erstellen und mit den Parametern zu konfigurieren, die an die Modifikator-Factory übergeben wurden.
  2. update: Diese Funktion wird immer dann aufgerufen, wenn dieser Modifikator an derselben Stelle angegeben wird, an der dieser Knoten bereits vorhanden ist, aber eine Eigenschaft geändert wurde. Dies wird durch die equals-Methode der Klasse bestimmt. Der zuvor erstellte Modifier-Knoten wird als Parameter an den update-Aufruf gesendet. Aktualisieren Sie nun die Knoteneigenschaften entsprechend den aktualisierten Parametern. Die Möglichkeit, Knoten auf diese Weise wiederverwendet zu werden, ist entscheidend für die Leistungssteigerungen, die Modifier.Node mit sich bringen kann. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt einen neuen Knoten in der Methode update zu erstellen. In unserem Beispiel mit dem Kreis wird die Farbe des Knotens aktualisiert.

Außerdem müssen ModifierNodeElement-Implementierungen auch equals und hashCode implementieren. update wird nur aufgerufen, wenn ein Gleichheitsvergleich mit dem vorherigen Element „false“ zurückgibt.

Im Beispiel oben wird dazu eine Datenklasse verwendet. Mit diesen Methoden wird geprüft, ob ein Knoten aktualisiert werden muss. Wenn Ihr Element Eigenschaften hat, die nicht dazu beitragen, ob ein Knoten aktualisiert werden muss, oder Sie Datenklassen aus Gründen der Binärkompatibilität vermeiden möchten, können Sie equals und hashCode manuell implementieren, z.B. das Element „padding modifier“.

Modifikator-Factory

Das ist die öffentliche API-Oberfläche deines Modifiers. Bei den meisten Implementierungen wird das Modifikatorelement einfach erstellt und der Modifikatorkette hinzugefügt:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

Vollständiges Beispiel

Aus diesen drei Teilen entsteht der benutzerdefinierte Modifikator, mit dem ein Kreis mithilfe der Modifier.Node APIs gezeichnet wird:

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

Häufige Situationen bei der Verwendung von Modifier.Node

Wenn Sie benutzerdefinierte Modifikatoren mit Modifier.Node erstellen, kann es zu folgenden häufigen Situationen kommen.

Null Parameter

Wenn Ihr Modifikator keine Parameter hat, muss er nie aktualisiert werden und muss außerdem keine Datenklasse sein. Hier ist eine Beispielimplementierung eines Modifiers, der einem Composeable einen festen Abstand hinzufügt:

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

Auf lokale Variablen in der Komposition verweisen

Modifier.Node-Modifikatoren berücksichtigen nicht automatisch Änderungen an Objekten des Erstellungsstatus (z. B. CompositionLocal). Der Vorteil von Modifier.Node Modifikatoren gegenüber Modifikatoren, die gerade mit einer zusammensetzbaren Factory erstellt wurden, besteht darin, dass sie den Wert der Komposition lokal auslesen können, wo der Modifikator in Ihrem UI-Baum verwendet wird, und nicht, wo der Modifikator zugewiesen ist. Dazu verwenden sie currentValueOf.

Instanzen mit Modifikatorknoten berücksichtigen jedoch Statusänderungen nicht automatisch. Wenn Sie automatisch auf eine lokale Änderung der Zusammensetzung reagieren möchten, können Sie den aktuellen Wert innerhalb eines Bereichs lesen:

In diesem Beispiel wird der Wert von LocalContentColor beobachtet, um einen Hintergrund anhand seiner Farbe zu zeichnen. Da ContentDrawScope Snapshot-Änderungen beobachtet, wird dies automatisch neu gezeichnet, wenn sich der Wert von LocalContentColor ändert:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Wenn Sie auf Statusänderungen außerhalb eines Gültigkeitsbereichs reagieren und den Modifikator automatisch aktualisieren möchten, verwenden Sie einen ObserverModifierNode.

Modifier.scrollable verwendet diese Methode beispielsweise, um Änderungen an LocalDensity zu beobachten. Hier ein vereinfachtes Beispiel:

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

Animierter Modifikator

Implementierungen von Modifier.Node haben Zugriff auf ein coroutineScope. Dadurch können die Compose Animatable APIs verwendet werden. In diesem Snippet wird beispielsweise CircleNode aus dem Beispiel oben so geändert, dass es wiederholt ein- und ausgeblendet wird:

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

Status zwischen Modifikatoren mithilfe der Delegation teilen

Modifier.Node-Modifikatoren können an andere Knoten delegiert werden. Dafür gibt es viele Anwendungsfälle, z. B. das Extrahieren gängiger Implementierungen in verschiedenen Modifikatoren. Es kann aber auch verwendet werden, um einen gemeinsamen Status für alle Modifikatoren zu erhalten.

Hier ein Beispiel für eine grundlegende Implementierung eines anklickbaren Modifier-Knotens, der Interaktionsdaten teilt:

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

Automatische Invalidation von Knoten deaktivieren

Modifier.Node-Knoten werden automatisch entwertet, wenn die entsprechenden ModifierNodeElement-Aufrufe aktualisiert werden. Bei einem komplexeren Modifikator kann es sinnvoll sein, dieses Verhalten zu deaktivieren, um genauer steuern zu können, wann der Modifikator Phasen ungültig macht.

Dies kann besonders nützlich sein, wenn der benutzerdefinierte Modifikator sowohl das Layout als auch die Zeichnung ändert. Wenn Sie die automatische Invalidation deaktivieren, können Sie das Zeichnen nur dann ungültig machen, wenn sich nur zeichnungsbezogene Eigenschaften wie color ändern, und das Layout nicht ungültig machen. Dadurch lässt sich die Leistung des Modifiers verbessern.

Unten sehen Sie ein hypothetisches Beispiel mit einem Modifikator, der die Lambdas color, size und onClick als Eigenschaften hat. Mit diesem Modifikator werden nur die erforderlichen Anforderungen ungültig gemacht und alle nicht erforderlichen Anforderungen übersprungen:

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