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 <ph type="x-smartling-placeholder">
      </ph>
    • Dies ist eine Erweiterungsfunktion für Modifier, die eine idiomatische API bereitstellt. für Ihren Modifikator ein und ermöglicht die einfache Verkettung von Modifikatoren. Die Modifikator-Factory erzeugt die Modifikatorelemente, die von Compose zum Ändern Ihrer Benutzeroberfläche.
  • Modifikatorelement <ph type="x-smartling-placeholder">
      </ph>
    • Hier können Sie das Verhalten des Modifikators implementieren.

Es gibt mehrere Möglichkeiten, einen benutzerdefinierten Modifizierer zu implementieren, je nachdem, Funktionalität erforderlich sind. Häufig lässt sich ein benutzerdefinierter Modifizierer am einfachsten um eine benutzerdefinierte Modifikator-Factory zu implementieren, die andere bereits definierte Modifikatoren kombiniert. Modifikator-Faktoren zu kombinieren. Wenn Sie benutzerdefiniertes Verhalten benötigen, implementieren Sie den Modifikatorelement verwenden. Diese befinden sich auf einer niedrigeren Ebene, Modifier.Node bieten mehr Flexibilität.

Vorhandene Modifikatoren verketten

Häufig ist es möglich, benutzerdefinierte Modifizierer allein durch die Verwendung vorhandener Modifizierern. Beispielsweise wird Modifier.clip() mithilfe des graphicsLayer-Modifikator. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet. Ihre eigene Factory mit benutzerdefinierten Modifikatoren bereitstellen.

Bevor Sie einen eigenen benutzerdefinierten Modifizierer implementieren, prüfen Sie, ob Sie denselben .

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

Wenn Sie feststellen, dass Sie dieselbe Gruppe von Modifizierern häufig wiederholen, können Sie in eigene Modifikatoren einfügen:

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 zu übergeben. mit einem vorhandenen Modifikator. Dies wird als zusammensetzbare Modifikatorfabrik bezeichnet.

Die Verwendung einer zusammensetzbaren Modifikator-Factory zum Erstellen eines Modifikators ermöglicht auch die Verwendung von übergeordnete Editoren-APIs, z. B. animate*AsState und andere Compose-APIs zustandsorientierte Animations-APIs. Das folgende Snippet zeigt beispielsweise 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 die bequeme Methode zur Bereitstellung von Standardwerten aus einem CompositionLocal lässt sich dies am einfachsten mit einer zusammensetzbaren Funktion implementieren 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 im Folgenden beschrieben werden.

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

Wenn Sie einen benutzerdefinierten Modifizierer mit einer Factory mit zusammensetzbaren Modifikatoren erstellen, Die Einheimischen berücksichtigen den Wert aus dem Kompositionsbaum, in dem sie erstellt wurden, verwendet. Dies kann zu unerwarteten Ergebnissen führen. Nehmen wir zum Beispiel die Komposition Beispiel für einen lokalen Modifikator (siehe oben), der etwas anders implementiert wurde mit einem zusammensetzbare Funktion:

@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 ein benutzerdefiniertes Modifier.Node, da lokale Kompositionen am Einsatzort korrekt beseitigt und kann sicher hochgezogen werden.

Zusammensetzbare Funktionsmodifikatoren werden nie übersprungen

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

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

Wie alle zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifikator aus der innerhalb der Komposition. Dadurch wird begrenzt, wohin ein Modifikator so hochgezogen werden kann, dürfen niemals aus der Komposition gezogen werden. Nicht zusammensetzbare Modifizierer Fabriken können aus zusammensetzbaren Funktionen gezogen werden, um die Wiederverwendung zu vereinfachen und Leistung 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. Es ist dieselbe API, in der Compose seine eigenen Modifikatoren implementiert, und die am stärksten benutzerdefinierte Modifikatoren erstellen.

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 Status des Modifikators.
  • Ein ModifierNodeElement, das Modifikatoren erstellt und aktualisiert Knoteninstanzen.
  • Eine optionale Modifikator-Factory wie oben beschrieben.

ModifierNodeElement Klassen sind zustandslos und jede neue Instanz wird zugewiesen Zusammensetzung, während Modifier.Node-Klassen zustandsorientiert sein können und für mehrere Neuzusammensetzungen verwendet und sogar wiederverwendet werden können.

Der folgende Abschnitt beschreibt die einzelnen Teile und zeigt ein Beispiel für den Aufbau einer benutzerdefinierten Modifikator, um einen Kreis zu zeichnen.

Modifier.Node

Mit der Modifier.Node-Implementierung (in diesem Beispiel CircleNode) wird die des 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 an den Modifikator übergebenen Farbe gezeichnet. .

Ein Knoten implementiert Modifier.Node sowie null oder mehr Knotentypen. Es gibt unterschiedliche Knotentypen erstellen, je nachdem, welche Funktionalität der Modifikator benötigt. Die im Beispiel oben muss gezeichnet werden können. Daher wird DrawModifierNode implementiert, ermöglicht das Überschreiben der Zeichenmethode.

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 ein Update auf dem entsprechenden Knoten aufgerufen wird. -Elements. Da es sich bei unserem Beispiel um eine DrawModifierNode handelt, wird jedes Mal ein Update am das Element enthält, löst der Knoten eine Neuzeichnung aus und seine Farbe wird korrekt aktualisiert. Es ist Möglichkeit, die automatische Entwertung zu deaktivieren, wie unten beschrieben.

ModifierNodeElement

Eine ModifierNodeElement ist eine unveränderliche Klasse, die die zu erstellenden oder zu erstellenden Daten enthält. aktualisieren Sie den benutzerdefinierten Modifikator:

// 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. Dadurch erhalten Sie wird aufgerufen, um den Knoten zu erstellen, wenn Ihr Modifikator zum ersten Mal angewendet wird. Normalerweise den Knoten zu erstellen und mit den Parametern zu konfigurieren, an die Modifikatorfabrik übergeben wurden.
  2. update: Diese Funktion wird immer dann aufgerufen, wenn dieser Modifikator im Dieser Knoten ist bereits vorhanden, aber eine Eigenschaft hat sich geändert. Dies ist der equals-Methode der Klasse bestimmt. Der Modifikatorknoten, der wird als Parameter an den update-Aufruf gesendet. An dieser Stelle sollten Sie den Wert der Knoten Eigenschaften, die den aktualisierten Parameter. Knoten können auf diese Weise wiederverwendet werden, die Leistungssteigerung durch Modifier.Node; Daher müssen Sie die vorhandenen Knoten zu erstellen, anstatt einen neuen Knoten in der Methode update zu erstellen. In unserem Kreis hinzugefügt, wird die Farbe des Knotens aktualisiert.

Außerdem muss bei ModifierNodeElement-Implementierungen equals implementiert werden. und hashCode. update wird nur aufgerufen, wenn ein Gleichheitsvergleich mit dem Das vorherige Element gibt false zurück.

Im obigen Beispiel wird dies mithilfe einer Datenklasse erreicht. Diese Methoden dienen dazu, ob ein Knoten aktualisiert werden muss. Wenn Ihr Element Eigenschaften aufweist, die tragen nicht dazu bei, ob ein Knoten aktualisiert werden muss oder Sie Daten vermeiden möchten. aus Gründen der binären Kompatibilität, dann können Sie equals manuell implementieren und hashCode z.B. das Element mit dem Modifikator für den Innenrand.

Modifikatorfabrik

Dies ist die öffentliche API-Oberfläche des Modifikators. Die meisten Implementierungen das Modifikatorelement erstellen und zur Modifikatorkette hinzufügen:

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

Vollständiges Beispiel

Diese drei Teile ergeben den benutzerdefinierten Modifikator zum Zeichnen eines Kreises. mithilfe der 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

Wenn Sie benutzerdefinierte Modifikatoren mit Modifier.Node erstellen, können folgende Situationen auftreten, begegnen können.

Nullparameter

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

Auf lokale Kompositionen verweisen

Modifier.Node-Modifikatoren berücksichtigen Änderungen am Erstellungsstatus nicht automatisch Objekte wie CompositionLocal. Die Vorteile von Modifier.Node-Modifikatoren gegenüber Modifikatoren, die gerade mit einer zusammensetzbaren Factory erstellt werden, sind, dass sie Wert der lokalen Zusammensetzung, von der aus der Modifikator in Ihrer Benutzeroberfläche verwendet wird Baumstruktur dargestellt, nicht dort, wo der Modifikator zugewiesen ist. Verwenden Sie dazu currentValueOf.

Instanzen mit Modifikatorknoten berücksichtigen jedoch Statusänderungen nicht automatisch. Bis automatisch auf lokale Änderungen der Komposition reagiert, innerhalb eines Bereichs:

In diesem Beispiel wird der Wert von LocalContentColor beobachtet, um einen hintergrundbasierten an seiner Farbe. Bei der Beobachtung von Snapshot-Änderungen durch ContentDrawScope 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()
    }
}

Um auf Statusänderungen außerhalb eines Bereichs zu reagieren und Ihre verwenden möchten, verwenden Sie ObserverModifierNode.

In Modifier.scrollable wird dieses Verfahren beispielsweise verwendet, um Veränderungen bei LocalDensity 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. So können Sie Compose Animatable APIs verwenden. Mit diesem Snippet wird beispielsweise die CircleNode von oben zum wiederholten Ein- und Ausblenden:

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. Es gibt viele Anwendungsfälle, Dazu gehört das Extrahieren gängiger Implementierungen für verschiedene Modifikatoren, aber es kann auch verwendet werden, um einen gemeinsamen Status für alle Modifikatoren zu erhalten.

Eine einfache Implementierung eines anklickbaren Modifikatorknotens, der 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 ihre entsprechenden Aktualisierung der ModifierNodeElement-Aufrufe. Manchmal können Sie bei einem komplexeren Modifikator dieses Verhalten deaktivieren möchten, um genauer steuern zu können, wann dass Ihr Modifizierer Phasen ungültig macht.

Dies ist besonders nützlich, wenn Ihr benutzerdefinierter Modifizierer sowohl das Layout Zeichnen. Wenn Sie die automatische Entwertung deaktivieren, können Sie die Zeichnung entwerten, wenn nur zeichnungsbezogene Eigenschaften wie color ändern und das Layout nicht ungültig machen. Dadurch kann die Leistung des Modifizierers verbessert werden.

Ein hypothetisches Beispiel dafür finden Sie unten mit einem Modifikator mit einem color, size und onClick Lambda als Properties. Durch diesen Modifikator wird nur das ungültig erforderlich. Dabei werden alle Entwertungen übersprungen, die den folgenden Kriterien entsprechen:

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