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.
- Dies ist eine Erweiterungsfunktion für
- 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 |
Ein |
||
Ein |
||
Durch die Implementierung dieser Schnittstelle kann Ihr |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
|
||
Eine Dies kann nützlich sein, um mehrere Knotenimplementierungen zu einer zusammenzufassen. |
||
Ermöglicht es |
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:
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.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 dieequals
-Methode der Klasse bestimmt. Der zuvor erstellte Änderungsknoten wird als Parameter an denupdate
-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, dieModifier.Node
bietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt einen neuen in derupdate
-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:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
&IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
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) } } }