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 Modifikator-Factory
    • Dies ist eine Erweiterungsfunktion von Modifier, die eine idiomatische API für Ihren Modifikator bietet und es ermöglicht, Modifikatoren einfach zu verketten. Die Modifikator-Factory erstellt die Modifikatorelemente, die von Compose zum Ändern der Benutzeroberfläche 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 Modifikator graphicsLayer implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifikator-Factory bereit.

Bevor Sie einen 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 mit einer composable modifier factory 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 praktische Methode ist, Standardwerte aus einer CompositionLocal anzugeben, ist die Verwendung einer kombinierbaren Modifikator-Factory die einfachste Implementierungsmethode:

@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 an der Aufrufstelle der Modifikator-Fabrik aufgelöst.

Wenn Sie einen benutzerdefinierten Modifikator mit einer composable modifier factory erstellen, nehmen Zusammensetzungs-Locals den Wert aus dem Kompositionbaum an, 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

Modifikatoren für kombinierbare Fabriken werden nie übersprungen, da kombinierbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifizierfunktion bei jeder Neuzusammensetzung aufgerufen wird. Das kann bei häufigen Neuzusammensetzungen kostspielig sein.

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 Vergleich dazu können nicht kombinierbare Modifikator-Fabriken aus kombinierbaren Funktionen ausgelagert 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
}

Benutzerdefiniertes Modifikatorverhalten mit Modifier.Node implementieren

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

Benutzerdefinierten Modifikator mit Modifier.Node implementieren

Die Implementierung eines benutzerdefinierten Modifiers mit Modifier.Node umfasst drei Teile:

  • Eine Modifier.Node-Implementierung, die die Logik und den Status des Modifiers enthält.
  • Ein ModifierNodeElement, mit dem Modifikatorknoteninstanzen erstellt und aktualisiert werden.
  • 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 und ü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 Modusfunktion übergeben wird.

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 hineinragt.

Beispiel

CompositionLocalConsumerModifierNode

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

Beispiel

SemanticsModifierNode

Ein Modifier.Node, das einen semantischen Schlüssel/Wert für die Verwendung bei Tests, Barrierefreiheit und ähnlichen Anwendungsfällen 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.Nodes, die ObserverNode implementieren, können eine eigene Implementierung von onObservedReadsChanged 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 in unserem Beispiel um ein DrawModifierNode handelt, wird jedes Mal, wenn das Element aktualisiert wird, ein Neuzeichnen ausgelöst und die Farbe wird korrekt aktualisiert. Sie können die automatische Invalidation 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 aufgerufen, wenn dieser Modifikator an derselben Stelle angegeben wird, an der dieser Knoten bereits vorhanden ist, sich aber eine Eigenschaft geändert hat. 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 so, dass sie den aktualisierten Parametern entsprechen. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerung, die Modifier.Node bietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt mit der Methode update einen neuen 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 Vergleich mit dem vorherigen Element den Wert „falsch“ zurückgibt.

Im Beispiel oben wird dazu eine Datenklasse verwendet. Mit diesen Methoden wird geprüft, ob ein Knoten aktualisiert werden muss oder nicht. 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 Anwendungsfälle für 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 auch 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 beobachten nicht automatisch Änderungen an Compose-Statusobjekten wie CompositionLocal. Der Vorteil von Modifier.Node-Modifikator gegenüber Modifikatoren, die nur mit einer composable factory erstellt werden, besteht darin, dass der Wert der Komposition lokal an der Stelle gelesen werden kann, an der der Modifizierer im UI-Baum verwendet wird, nicht an der Stelle, an der er zugewiesen wird, mit currentValueOf.

Modifizierknoteninstanzen beobachten jedoch nicht automatisch Statusänderungen. Wenn du automatisch auf eine lokale Änderung einer Komposition reagieren möchtest, kannst du den aktuellen Wert innerhalb eines Gültigkeitsbereichs lesen:

In diesem Beispiel wird der Wert von LocalContentColor verwendet, um einen Hintergrund basierend auf seiner Farbe zu zeichnen. Da ContentDrawScope Snapshot-Änderungen beobachtet, wird das Diagramm 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 Bereichs reagieren und den Modifikator automatisch aktualisieren möchten, verwenden Sie eine ObserverModifierNode.

Modifier.scrollable verwendet diese Methode beispielsweise, um Änderungen an LocalDensity zu beobachten. Unten sehen Sie 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

Modifier.Node-Implementierungen haben Zugriff auf eine 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. Es gibt viele Anwendungsfälle dafür, z. B. das Extrahieren gemeinsamer Implementierungen für verschiedene Modifikatoren. Es kann aber auch verwendet werden, um einen gemeinsamen Status für verschiedene Modifikatoren zu teilen.

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 ungültig, wenn die entsprechenden ModifierNodeElement-Aufrufe aktualisiert werden. Bei komplexeren Modifikatoren kann es jedoch sinnvoll sein, dieses Verhalten zu deaktivieren, um genauer festlegen zu können, wann Phasen durch den Modifikator ungültig werden.

Das kann besonders nützlich sein, wenn Ihr benutzerdefinierter Modifikator sowohl das Layout als auch das Zeichnen ä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)
        }
    }
}