Benutzerdefinierte Modifikatoren erstellen

Mit der Funktion „Compose“ können Sie sofort viele Modifikatoren für gängige Verhaltensweisen verwenden. Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.

Modifikatoren bestehen aus mehreren Teilen:

  • Eine Modifikatorfabrik
    • Dies ist eine Erweiterungsfunktion für Modifier, die eine idiomatische API für den Modifikator bereitstellt und Modifikatoren einfach verkettet. Die Modifizierer-Factory erzeugt die Modifikatorelemente, die von Compose zum Ändern der UI verwendet werden.
  • Ein Modifikatorelement
    • Hier können Sie das Verhalten des Modifizierers implementieren.

Es gibt mehrere Möglichkeiten, einen benutzerdefinierten Modifikator zu implementieren, abhängig von der benötigten Funktionalität. Häufig lässt sich ein benutzerdefinierter Modifikator am einfachsten implementieren, indem eine Factory Factory mit benutzerdefinierten Modifikatoren kombiniert wird. Wenn Sie benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifiziererelement mithilfe der Modifier.Node APIs. Diese APIs sind niedriger, bieten aber mehr Flexibilität.

Vorhandene Modifikatoren verketten

Häufig ist es möglich, benutzerdefinierte Modifikatoren einfach mithilfe vorhandener Modifikatoren zu erstellen. Beispielsweise wird Modifier.clip() mit dem graphicsLayer-Modifikator implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene Factory für benutzerdefinierte Modifikatoren bereit.

Bevor Sie Ihren 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 einbinden:

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

Benutzerdefinierten Modifikator mithilfe einer Factory für zusammensetzbare Modifikatoren erstellen

Sie können auch einen benutzerdefinierten Modifikator mit einer zusammensetzbaren Funktion erstellen, um Werte an einen vorhandenen Modifikator zu übergeben. Dies wird als „zusammensetzbare Modifikatorfabrik“ bezeichnet.

Wenn Sie eine Modifikatorfabrik mit zusammensetzbaren Modifikatoren zum Erstellen eines Modifikators verwenden, können auch übergeordnete APIs wie animate*AsState und andere APIs mit Compose-Status-gestützten Animationen verwendet werden. Das folgende Snippet zeigt beispielsweise einen Modifizierer, der bei Aktivierung/Deaktivierung eine Alphaänderung 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 }
}

return graphicsLayer { this.alpha = alpha }

Wenn der benutzerdefinierte Modifikator eine praktische Methode zur Bereitstellung von Standardwerten aus einem CompositionLocal ist, lässt sich dies am einfachsten mit einer zusammensetzbaren Modifizierer-Factory implementieren:

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

Bei diesem Ansatz gibt es einige Einschränkungen, die im Folgenden erläutert werden.

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

Wenn Sie einen benutzerdefinierten Modifikator mit einer Factory für zusammensetzbare Modifikatoren erstellen, übernehmen Kompositionslokale den Wert aus der Zusammensetzungsstruktur, in der sie erstellt wurden, und nicht verwendet. Dies kann zu unerwarteten Ergebnissen führen. Nehmen wir z. B. das obige Beispiel für den lokalen Kompositionsmodifikator, der mit einer zusammensetzbaren 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 dies nicht die erwartete Funktionsweise des Modifizierers ist, verwenden Sie stattdessen einen benutzerdefinierten Modifier.Node, da Kompositionslokale am Nutzungsort korrekt aufgelöst werden und sicher hochgezogen werden können.

Zusammensetzbare Funktionsmodifikatoren werden nie übersprungen

Zusammensetzbare Fabrikmodifikatoren werden niemals übersprungen, da zusammensetzbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass die Modifikatorfunktion bei jeder Neuzusammensetzung aufgerufen wird, was teuer werden kann, wenn sie häufig neu zusammensetzt.

Modifikatoren für zusammensetzbare Funktionen müssen innerhalb einer zusammensetzbaren Funktion aufgerufen werden

Wie bei allen zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifikator aus der Zusammensetzung aufgerufen werden. Dies schränkt ein, wohin ein Modifikator hochgezogen werden kann, da er niemals aus der Komposition hochgezogen werden kann. Im Gegensatz dazu können nicht zusammensetzbare Modifikatorfabriken aus zusammensetzbaren Funktionen entfernt werden, um eine einfachere Wiederverwendung zu ermöglichen 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
}

Verhalten von benutzerdefinierten Modifikatoren mit Modifier.Node implementieren

Modifier.Node ist eine Low-Level-API zum Erstellen von Modifikatoren in der Funktion „Compose“. Es ist die API, in der Compose seine eigenen Modifikatoren implementiert. Sie ist die leistungsstärkste Methode, benutzerdefinierte Modifikatoren zu erstellen.

Benutzerdefinierten Modifikator mit Modifier.Node implementieren

Die Implementierung eines benutzerdefinierten Modifikators mit Modifier.Node umfasst drei Schritte:

  • Eine Modifier.Node-Implementierung, die die Logik und den Status des Modifizierers enthält.
  • Ein ModifierNodeElement, das Modifikatorknoteninstanzen erstellt und aktualisiert.
  • Eine optionale Modifikatorfabrik, wie oben beschrieben.

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

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

Modifier.Node

Mit der Implementierung Modifier.Node (in diesem Beispiel CircleNode) wird die Funktionalität des benutzerdefinierten Modifikators implementiert.

// 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. Es gibt verschiedene Knotentypen, je nachdem, welche Funktionalität Ihr Modifikator erfordert. Das obige Beispiel muss in der Lage sein, zu zeichnen. Deshalb wird DrawModifierNode implementiert, mit dem die „draw“-Methode überschrieben werden kann.

Folgende Typen sind verfügbar:

Knoten

Verwendung

Beispiellink

LayoutModifierNode

Ein Modifier.Node, das ändert, wie der umschlossene Inhalt gemessen und dargestellt wird.

Beispiel

DrawModifierNode

Ein Modifier.Node, das den Raum des Layouts einzieht.

Beispiel

CompositionLocalConsumerModifierNode

Wenn diese Schnittstelle implementiert wird, kann der Modifier.Node Kompositionslokale 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

Ein Modifier.Node, der PointerInputChanges. empfängt.

Beispiel

ParentDataModifierNode

Ein Modifier.Node, das Daten für das übergeordnete Layout bereitstellt.

Beispiel

LayoutAwareModifierNode

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

Beispiel

GlobalPositionAwareModifierNode

Ein Modifier.Node, der einen onGloballyPositioned-Callback mit der letzten LayoutCoordinates des Layouts empfängt, wenn sich die globale Position des Inhalts geändert hat.

Beispiel

ObserverModifierNode

Modifier.Nodes, die ObserverNode implementieren, können eine eigene Implementierung von onObservedReadsChanged bereitstellen, die als Reaktion auf Änderungen an Snapshot-Objekten aufgerufen wird, die innerhalb eines observeReads-Blocks gelesen werden.

Beispiel

DelegatingNode

Ein Modifier.Node, der Arbeit an andere Modifier.Node-Instanzen delegieren kann.

Dies kann nützlich sein, wenn Sie mehrere Knotenimplementierungen zu einer zusammenfassen möchten.

Beispiel

TraversableNode

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

Beispiel

Knoten werden automatisch entwertet, wenn eine Aktualisierung für ihr entsprechendes Element aufgerufen wird. Da es sich bei unserem Beispiel um DrawModifierNode handelt, löst jedes Mal, wenn das Element aktualisiert wird, eine Neuzeichnung aus und die Farbe wird korrekt aktualisiert. Sie können die automatische Entwertung wie unten beschrieben deaktivieren.

ModifierNodeElement

Ein ModifierNodeElement ist eine unveränderliche Klasse, die die Daten enthält, die den benutzerdefinierten Modifikator erstellen oder aktualisieren sollen:

// 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: Dies ist die Funktion, die den Modifikatorknoten instanziiert. Diese wird aufgerufen, um den Knoten zu erstellen, wenn der Modifikator zum ersten Mal angewendet wird. In der Regel umfasst dies die Erstellung des Knotens und dessen Konfiguration mit den Parametern, die an die Modifikator-Factory übergeben wurden.
  2. update: Diese Funktion wird immer dann aufgerufen, wenn sich dieser Modifikator an derselben Stelle befindet, an der dieser Knoten bereits vorhanden ist, sich aber ein Attribut geändert hat. Dies wird durch die Methode equals der Klasse bestimmt. Der zuvor erstellte Modifikatorknoten wird als Parameter an den Aufruf update gesendet. An dieser Stelle sollten Sie die Eigenschaften der Knoten so aktualisieren, dass sie den aktualisierten Parametern entsprechen. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerungen, die Modifier.Node mit sich bringt. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt in der Methode update einen neuen zu erstellen. In unserem Kreisbeispiel wird die Farbe des Knotens aktualisiert.

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

Im obigen Beispiel wird dazu eine Datenklasse verwendet. Mit diesen Methoden wird geprüft, ob ein Knoten aktualisiert werden muss. Wenn das Element Eigenschaften hat, die nicht dazu beitragen, dass 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 Padding-Modifikatorelement.

Modifizierer Werkseinstellung

Dies ist die öffentliche API-Oberfläche Ihres Modifizierers. Bei den meisten Implementierungen wird einfach das Modifikatorelement erstellt und in die Modifikatorkette aufgenommen:

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

Vollständiges Beispiel

Diese drei Teile ergeben zusammen den benutzerdefinierten Modifikator, mit dem mit den Modifier.Node APIs ein Kreis 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 Verwendung von Modifier.Node

Wenn Sie benutzerdefinierte Modifikatoren mit Modifier.Node erstellen, können im Folgenden einige gängige Situationen auftreten.

Null Parameter

Hat der Modifikator keine Parameter, muss er nicht aktualisiert werden und muss außerdem keine Datenklasse sein. Hier sehen Sie ein Beispiel für die Implementierung eines Modifikators, bei dem ein fester Wert für das Padding auf eine zusammensetzbare Funktion angewendet wird:

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

Verweise auf lokale Kompositionen

Modifier.Node-Modifikatoren beobachten nicht automatisch Änderungen an Objekten für den Status „Compose“, z. B. CompositionLocal. Der Vorteil von Modifier.Node-Modifikatoren gegenüber Modifikatoren, die gerade mit einer zusammensetzbaren Factory erstellt wurden, besteht darin, dass sie mit currentValueOf den Wert der lokalen Komposition aus dem Ort lesen können, an dem der Modifikator im UI-Baum verwendet wird, nicht dort, wo der Modifikator zugewiesen ist.

Instanzen von Modifikatorknoten beobachten jedoch Statusänderungen nicht automatisch. Wenn Sie automatisch auf eine Änderung der lokalen 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 basierend auf seiner Farbe zu zeichnen. Da ContentDrawScope Snapshot-Änderungen erkennt, wird dies automatisch neu erstellt, 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 Bereichs reagieren und den Modifizierer automatisch aktualisieren möchten, verwenden Sie einen ObserverModifierNode.

In Modifier.scrollable wird dieses Verfahren beispielsweise verwendet, um Änderungen in 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)
    }
}

Animierender Modifikator

Modifier.Node-Implementierungen haben Zugriff auf eine coroutineScope. Dies ermöglicht die Verwendung der Compose Animatable APIs. Dieses Snippet ändert beispielsweise die oben angegebene CircleNode so, dass sie 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 durch Delegierung zwischen Modifikatoren freigeben

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

Hier ein Beispiel für eine einfache Implementierung eines anklickbaren Modifikatorknotens mit gemeinsamen Interaktionsdaten:

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

Automatische Knotenentwertung deaktivieren

Modifier.Node-Knoten werden automatisch entwertet, wenn die entsprechenden ModifierNodeElement-Aufrufe aktualisiert werden. Bei komplexeren Modifikatoren 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 mit dem benutzerdefinierten Modifikator sowohl das Layout als auch die Zeichnung geändert werden. Wenn Sie die automatische Entwertung deaktivieren, können Sie eine Zeichnung nur dann ungültig machen, wenn nur zeichnenbezogene Eigenschaften wie color geändert oder das Layout nicht ungültig wird. Dies kann die Leistung des Modifizierers verbessern.

Unten sehen Sie ein hypothetisches Beispiel mit einem Modifikator, der die Lambda-Eigenschaften color, size und onClick hat. Dieser Modifikator macht nur erforderliche Entwertungen und überspringt alle Entwertungen, die nicht:

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