Benutzerdefinierte Modifikatoren erstellen

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

Modifikatoren bestehen aus mehreren Teilen:

  • Eine Modifikator-Factory
    • Dies ist eine Erweiterungsfunktion für Modifier, die eine idiomatische API für Ihren Modifier bietet und es ermöglicht, Modifier einfach zu verketten. Die Modifikator-Factory erzeugt die Modifikatorelemente, die von Compose zum Ändern der Benutzeroberfläche verwendet werden.
  • Ein Modifikatorelement
    • Hier können Sie das Verhalten des Modifikators implementieren.

Es gibt mehrere Möglichkeiten, einen benutzerdefinierten Modifikator zu implementieren, je nach benötigter Funktionalität. Oft ist es am einfachsten, einen benutzerdefinierten Modifikator zu implementieren, indem Sie eine benutzerdefinierte Modifikator-Factory implementieren, die andere bereits definierte Modifikator-Factories kombiniert. Wenn Sie ein benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifiziererelement mit den Modifier.Node-APIs. Diese sind zwar auf niedrigerer Ebene, bieten aber mehr Flexibilität.

Vorhandene Modifikatoren verketten

Häufig lassen sich benutzerdefinierte Modifikatoren erstellen, indem Sie vorhandene Modifikatoren verwenden. Modifier.clip() wird beispielsweise mit dem Modifikator graphicsLayer implementiert. Bei dieser Strategie werden vorhandene Modifiziererelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifizierer-Factory bereit.

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

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

Wenn Sie häufig dieselbe Gruppe von Modifikatoren verwenden, können Sie sie in einem eigenen Modifikator zusammenfassen:

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

Benutzerdefinierten Modifier mit einer zusammensetzbaren Modifier-Factory erstellen

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

Wenn Sie einen zusammensetzbaren Modifikator verwenden, um einen Modifikator zu erstellen, können Sie auch Compose-APIs auf höherer Ebene verwenden, z. B. animate*AsState und andere Compose-Animations-APIs mit Status. Das folgende Snippet zeigt beispielsweise einen Modifier, der eine Änderung des Alphawerts animiert, wenn er aktiviert oder deaktiviert wird:

@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 Hilfsmethode zum Bereitstellen von Standardwerten aus einer CompositionLocal ist, ist die einfachste Möglichkeit zur Implementierung die Verwendung einer zusammensetzbaren Modifikator-Factory:

@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 am Aufrufort der Modifizierer-Factory aufgelöst.

Wenn Sie einen benutzerdefinierten Modifier mit einer zusammensetzbaren Modifier-Factory erstellen, übernehmen die Composition Locals den Wert aus dem Kompositionsbaum, in dem sie erstellt werden, nicht aus dem, in dem sie verwendet werden. Das kann zu unerwarteten Ergebnissen führen. Sehen wir uns beispielsweise das oben gezeigte Beispiel für einen lokalen Modifikator an, das mit einer zusammensetzbaren Funktion etwas anders implementiert wird:

@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 das nicht Ihren Erwartungen entspricht, verwenden Sie stattdessen eine benutzerdefinierte Modifier.Node, da Kompositionslokale am Verwendungsort richtig aufgelöst und sicher verschoben werden können.

Composable-Funktionsmodifikatoren werden nie übersprungen

Composable-Factory-Modifier werden nie übersprungen, da Composable-Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifier-Funktion bei jeder Neuzusammenstellung aufgerufen wird. Das kann teuer sein, wenn sie häufig neu zusammengestellt wird.

Composable-Funktionsmodifizierer müssen innerhalb einer Composable-Funktion aufgerufen werden.

Wie alle zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifier innerhalb der Komposition aufgerufen werden. Dadurch wird eingeschränkt, wohin ein Modifier verschoben werden kann, da er nie aus der Komposition verschoben werden kann. Im Vergleich dazu können nicht zusammensetzbare Modifikator-Factories aus zusammensetzbaren Funktionen herausgezogen werden, um die Wiederverwendung zu erleichtern 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 Modifier-Verhalten mit Modifier.Node implementieren

Modifier.Node ist eine API auf niedrigerer Ebene zum Erstellen von Modifizierern in Compose. Es ist dieselbe API, in der Compose eigene Modifizierer implementiert, und die leistungsstärkste Methode zum Erstellen benutzerdefinierter Modifizierer.

Benutzerdefinierten Modifikator mit Modifier.Node implementieren

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

  • Eine Modifier.Node-Implementierung, die die Logik und den Status Ihres Modifikators enthält.
  • Ein ModifierNodeElement, das Instanzen von Modifikator-Knoten erstellt und aktualisiert.
  • Eine optionale Modifizierer-Factory, wie oben beschrieben.

ModifierNodeElement-Klassen sind zustandslos und bei jeder Neuzusammenstellung werden neue Instanzen zugewiesen. Modifier.Node-Klassen können dagegen zustandsorientiert sein und mehrere Neuzusammenstellungen überdauern. Sie können sogar wiederverwendet werden.

Im folgenden Abschnitt wird jeder Teil beschrieben und es wird ein Beispiel für das Erstellen eines benutzerdefinierten Modifikators zum Zeichnen eines Kreises gezeigt.

Modifier.Node

Die Modifier.Node-Implementierung (in diesem Beispiel CircleNode) implementiert die Funktionalität Ihres benutzerdefinierten Modifikators.

// 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 der erforderlichen Funktionalität des Modifikators gibt es verschiedene Knotentypen. Das obige Beispiel muss in der Lage sein, etwas zu zeichnen. Daher wird DrawModifierNode implementiert, wodurch die Methode „draw“ überschrieben werden kann.

Die verfügbaren Typen sind:

Knoten

Verwendung

Beispiellink

LayoutModifierNode

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

Beispiel

DrawModifierNode

Ein Modifier.Node, das in den Bereich des Layouts gezeichnet wird.

Beispiel

CompositionLocalConsumerModifierNode

Durch die Implementierung dieser Schnittstelle kann Ihr Modifier.Node Kompositions-Locals lesen.

Beispiel

SemanticsModifierNode

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

Beispiel

PointerInputModifierNode

Ein Modifier.Node, das PointerInputChanges empfängt.

Beispiel

ParentDataModifierNode

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

Beispiel

LayoutAwareModifierNode

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

Beispiel

GlobalPositionAwareModifierNode

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

Beispiel

ObserverModifierNode

Modifier.Nodes, die ObserverNode implementieren, können ihre 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 Arbeit an andere Modifier.Node-Instanzen delegieren kann.

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

Beispiel

TraversableNode

Ermöglicht es Modifier.Node-Klassen, den Knotenbaum für Klassen desselben Typs oder für einen bestimmten Schlüssel aufwärts/abwärts zu durchlaufen.

Beispiel

Knoten werden automatisch ungültig, wenn die Aktualisierung für das entsprechende Element aufgerufen wird. Da unser Beispiel ein DrawModifierNode ist, wird bei jedem Aufruf von „update“ für das Element ein Neuzeichnen ausgelöst und die Farbe wird korrekt aktualisiert. Sie können die automatische Ungültigmachung deaktivieren. Weitere Informationen finden Sie unten.

ModifierNodeElement

Eine ModifierNodeElement ist eine unveränderliche Klasse, die die Daten zum Erstellen oder Aktualisieren Ihres 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, mit der der Modifikator-Knoten instanziiert wird. Diese Funktion wird aufgerufen, um den Knoten zu erstellen, wenn der Modifier zum ersten Mal angewendet wird. In der Regel besteht das darin, 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 Modifier 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 Änderungsknoten wird als Parameter an den update-Aufruf gesendet. An diesem Punkt sollten Sie die Attribute der Knoten entsprechend den aktualisierten Parametern aktualisieren. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerungen, die Modifier.Node bietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt einen neuen in der update-Methode 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 obigen Beispiel 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 wenn Sie aus Gründen der binären Kompatibilität Datenklassen vermeiden möchten, können Sie equals und hashCode manuell implementieren, z.B. das Element für den Padding-Modifikator.

Modifikator-Factory

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

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

Vollständiges Beispiel

Diese drei Teile bilden zusammen den benutzerdefinierten Modifier zum Zeichnen eines Kreises mit den Modifier.Node-APIs:

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

Hier sind einige häufige Situationen, die beim Erstellen benutzerdefinierter Modifikatoren mit Modifier.Node auftreten können.

Keine Parameter

Wenn Ihr Modifier keine Parameter hat, muss er nie aktualisiert werden und muss auch keine Datenklasse sein. Hier ist ein Beispiel für die Implementierung eines Modifiers, der einem Composable einen festen Abstand zuweist:

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

Lokale Variablen für Composables referenzieren

Modifier.Node-Modifikatoren berücksichtigen Änderungen an Compose-Zustandsobjekten wie CompositionLocal nicht automatisch. Der Vorteil von Modifier.Node-Modifikatoren gegenüber Modifikatoren, die nur mit einer zusammensetzbaren Factory erstellt werden, besteht darin, dass sie den Wert des Composition Local dort lesen können, wo der Modifikator in Ihrem UI-Baum verwendet wird, und nicht dort, wo der Modifikator zugewiesen wird, indem sie currentValueOf verwenden.

Instanzen von Modifikator-Knoten reagieren jedoch nicht automatisch auf Statusänderungen. Wenn Sie automatisch auf eine Änderung des lokalen Werts einer Komposition reagieren möchten, können Sie den aktuellen Wert in einem Bereich lesen:

In diesem Beispiel wird der Wert von LocalContentColor beobachtet, um einen Hintergrund basierend auf seiner Farbe zu zeichnen. Da ContentDrawScope Snapshot-Änderungen beobachtet, wird 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 Ihren Modifier automatisch aktualisieren möchten, verwenden Sie ein ObserverModifierNode.

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

Animierender Modifikator

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

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private lateinit var alpha: Animatable<Float, AnimationVector1D>

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        alpha = Animatable(1f)
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

Status zwischen Modifikatoren über Delegation teilen

Modifier.Node-Modifikatoren können an andere Knoten delegieren. Dafür gibt es viele Anwendungsfälle, z. B. das Extrahieren gemeinsamer Implementierungen für verschiedene Modifizierer. Es kann aber auch verwendet werden, um einen gemeinsamen Status für Modifizierer zu nutzen.

Hier ein Beispiel für eine einfache Implementierung eines klickbaren Modifier-Knotens, der Interaktionsdaten weitergibt:

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

Automatische Knotenungültigmachung deaktivieren

Modifier.Node-Knoten werden automatisch ungültig, wenn die entsprechenden ModifierNodeElement-Aufrufe aktualisiert werden. Bei komplexeren Modifikatoren kann es sinnvoll sein, dieses Verhalten zu deaktivieren, um genauer zu steuern, wann Phasen durch den Modifikator ungültig werden.

Das kann besonders nützlich sein, wenn Ihr benutzerdefinierter Modifier sowohl das Layout als auch das Zeichnen ändert. Wenn Sie die automatische Ungültigmachung deaktivieren, müssen Sie nur dann neu zeichnen, wenn sich nur zeichnungsbezogene Eigenschaften wie color ändern. Das Layout muss dann nicht neu berechnet werden. Dadurch kann die Leistung des Modifikators verbessert werden.

Ein hypothetisches Beispiel dafür ist unten zu sehen. Der Modifikator hat die Eigenschaften color, size und onClick als Lambda-Funktionen. Dieser Modifikator macht nur das ungültig, was erforderlich ist. Ungültigmachungen, die nicht erforderlich sind, werden ü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)
        }
    }
}