Benutzerdefinierte Modifikatoren erstellen

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

Modifikatoren bestehen aus mehreren Teilen:

  • 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 Modifikatorelement
    • Hier können Sie das Verhalten des Modifikators implementieren.

Je nach erforderlicher Funktionalität gibt es mehrere Möglichkeiten, einen benutzerdefinierten Modifizierer zu implementieren. Häufig besteht die einfachste Möglichkeit zur Implementierung eines benutzerdefinierten Modifikators darin, eine benutzerdefinierte Modifikator-Factory zu implementieren, die andere bereits definierte Modifikator-Factorys miteinander kombiniert. Wenn Sie mehr benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifikatorelement mithilfe der Modifier.Node APIs. Diese sind eine niedrigere Ebene, bieten aber mehr Flexibilität.

Vorhandene Modifikatoren verketten

Häufig ist es möglich, benutzerdefinierte Modifikatoren allein durch die Verwendung 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 Modifizierer 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 Ihren eigenen Modifikator einbinden:

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

Benutzerdefinierten Modifikator mithilfe einer Factory zusammensetzbaren 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 Factory mit zusammensetzbaren Modifikatoren verwenden, um einen Modifikator zu erstellen, können Sie auch übergeordnete Erstellungs-APIs wie animate*AsState und andere APIs im Erstellungsstatus der Animation verwenden. Das folgende Snippet zeigt beispielsweise einen Modifikator, der bei Aktivierung bzw. 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 }
}

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 im Folgenden beschrieben werden.

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

Wenn Sie einen benutzerdefinierten Modifikator mithilfe einer Factory mit zusammensetzbaren Modifikatoren erstellen, übernehmen lokale Kompositionen den Wert aus dem Zusammensetzungsbaum, in dem sie erstellt wurden, und werden nicht verwendet. Dies kann zu unerwarteten Ergebnissen führen. Nehmen wir als Beispiel das obige Beispiel für einen lokalen Modifikator für die Komposition, 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 der Modifikator nicht so funktioniert, verwenden Sie stattdessen ein benutzerdefiniertes Modifier.Node, da die lokalen Kompositionen an der Verwendungsstelle korrekt entfernt werden und sicher hochgezogen werden können.

Zusammensetzbare Funktionsmodifikatoren 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 zusammensetzbare Funktionen müssen innerhalb einer zusammensetzbaren Funktion aufgerufen werden

Wie alle zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifikator innerhalb der Zusammensetzung aufgerufen werden. Dadurch wird der Ort begrenzt, an den ein Modifikator hochgezogen werden kann, da er nie aus der Zusammensetzung gezogen 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
}

Verhalten benutzerdefinierter Modifikatoren 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 Modifikators enthält.
  • Einem ModifierNodeElement, das Knoteninstanzen mit Modifikator erstellt und aktualisiert.
  • Eine optionale Modifikator-Factory wie oben beschrieben.

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

Der folgende Abschnitt beschreibt die einzelnen Teile und zeigt ein Beispiel für das Erstellen eines benutzerdefinierten Modifikators zum Zeichnen eines Kreises.

Modifier.Node

Mit der Modifier.Node-Implementierung (in diesem Beispiel CircleNode) wird die Funktionalität des benutzerdefinierten Modifizierers 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. Je nach den für den Modifikator erforderlichen Funktionen gibt es verschiedene Knotentypen. Im Beispiel oben muss gezeichnet werden können. Deshalb wird DrawModifierNode implementiert, wodurch die Zeichenmethode überschrieben werden kann.

Folgende Typen sind verfügbar:

Knoten

Verwendung

Beispiellink

LayoutModifierNode

Ein Modifier.Node, das die Art und Weise ändert, wie sein umschlossener Inhalt gemessen und angeordnet wird.

Beispiel

DrawModifierNode

Ein Modifier.Node, das in den Bereich des Layouts einzieht.

Beispiel

CompositionLocalConsumerModifierNode

Durch die Implementierung dieser Schnittstelle kann dein Modifier.Node lokale Kompositionen 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.

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, das einen onGloballyPositioned-Callback mit dem letzten LayoutCoordinates-Wert des Layouts empfängt, wenn sich die globale Position des Inhalts 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

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

Dies kann nützlich sein, um mehrere Knotenimplementierungen in einer zusammenzufassen.

Beispiel

TraversableNode

Erlaubt Modifier.Node-Klassen, in der Knotenstruktur nach oben und unten nach Klassen desselben Typs oder für einen bestimmten Schlüssel zu gehen.

Beispiel

Knoten werden automatisch ungültig gemacht, wenn eine Aktualisierung für das entsprechende Element 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

Ein ModifierNodeElement ist eine unveränderliche Klasse, die die Daten zum Erstellen oder Aktualisieren des benutzerdefinierten Modifikators 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: Dies ist die Funktion, die den Modifikatorknoten instanziiert. Diese wird aufgerufen, um den Knoten zu erstellen, wenn der Modifikator zum ersten Mal angewendet wird. Normalerweise bedeutet dies, dass der Knoten erstellt und mit den Parametern konfiguriert wird, 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 Methode equals der Klasse bestimmt. Der zuvor erstellte Modifikatorknoten wird als Parameter an den update-Aufruf gesendet. An dieser Stelle sollten Sie die Attribute der Knoten entsprechend den aktualisierten Parametern aktualisieren. 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 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 Gleichheitsvergleich mit dem vorherigen Element „false“ zurückgibt.

Im obigen Beispiel wird dies mithilfe einer Datenklasse erreicht. Mit diesen Methoden wird geprüft, ob ein Knoten aktualisiert werden muss. Wenn Ihr Element Attribute hat, die nicht dazu beitragen, ob ein Knoten aktualisiert werden muss, oder Sie Datenklassen aus Gründen der binären Kompatibilität vermeiden möchten, können Sie equals und hashCode manuell implementieren, z.B. das Padding-Modifikatorelement.

Modifikatorfabrik

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

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

Vollständiges Beispiel

Diese drei Teile werden zusammengefügt, um den benutzerdefinierten Modifikator zum Zeichnen eines Kreises mit den Modifier.Node APIs zu erstellen:

// 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, sind hier einige häufige Situationen aufgeführt, die auftreten können.

Nullparameter

Wenn Ihr Modifikator keine Parameter hat, muss er nie aktualisiert werden und muss außerdem keine Datenklasse sein. Hier sehen Sie eine Beispielimplementierung eines Modifikators, mit dem auf eine zusammensetzbare Funktion ein fester Abstand 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)
        }
    }
}

Auf lokale Kompositionen 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 dort, wo der Modifikator zugewiesen ist. Verwenden Sie dazu 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()
    }
}

Verwenden Sie ObserverModifierNode, um auf Statusänderungen außerhalb eines Bereichs zu reagieren und den Modifikator automatisch zu aktualisieren.

Modifier.scrollable verwendet diese Technik beispielsweise, 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)
    }
}

Modifikator wird animiert

Implementierungen von Modifier.Node haben Zugriff auf ein coroutineScope. Dies ermöglicht die Verwendung der Compose Animatable APIs. Mit diesem Snippet wird das CircleNode-Objekt beispielsweise 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)
            ) {
            }
        }
    }
}

Freigabe des Status zwischen Modifikatoren durch Delegierung

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. Es kann aber auch verwendet werden, um einen gemeinsamen Status für alle Modifikatoren zu erhalten.

Eine einfache Implementierung eines anklickbaren Modifikatorknotens, der Interaktionsdaten teilt:

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 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 Entwertung deaktivieren, wird das Zeichnen nur dann ungültig, wenn nur zeichnungsbezogene Eigenschaften wie color das Layout ändern und nicht ungültig sind. Dadurch kann die Leistung des Modifizierers verbessert werden.

Ein hypothetisches Beispiel dafür finden Sie unten mit einem Modifikator mit den Lambda-Attributen color, size und onClick. Mit diesem Modifikator wird nur das entwertet, was erforderlich ist, und alle Entwertungen werden übersprungen, wenn dies Folgendes nicht zutrifft:

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