Mit der Funktion „Compose“ können Sie sofort viele Modifikatoren für gängige Verhaltensweisen verwenden. Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.
Modifikatoren bestehen aus mehreren Teilen:
- Eine Modifikatorfabrik
- Dies ist eine Erweiterungsfunktion für
Modifier
, die eine idiomatische API für den Modifikator bereitstellt und Modifikatoren einfach verkettet. Die Modifizierer-Factory erzeugt die Modifikatorelemente, die von Compose zum Ändern der UI verwendet werden.
- Dies ist eine Erweiterungsfunktion für
- Ein Modifikatorelement
- Hier können Sie das Verhalten des Modifizierers implementieren.
Es gibt mehrere Möglichkeiten, einen benutzerdefinierten Modifikator zu implementieren, abhängig von der benötigten Funktionalität. Häufig lässt sich ein benutzerdefinierter Modifikator am einfachsten implementieren, indem eine Factory Factory mit benutzerdefinierten Modifikatoren kombiniert wird. Wenn Sie benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifiziererelement mithilfe der Modifier.Node
APIs. Diese APIs sind niedriger, bieten aber mehr Flexibilität.
Vorhandene Modifikatoren verketten
Häufig ist es möglich, benutzerdefinierte Modifikatoren einfach mithilfe vorhandener Modifikatoren zu erstellen. Beispielsweise wird Modifier.clip()
mit dem graphicsLayer
-Modifikator implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene Factory für benutzerdefinierte Modifikatoren bereit.
Bevor Sie Ihren eigenen 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 einbinden:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Benutzerdefinierten Modifikator mithilfe einer Factory für zusammensetzbare 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 Modifikatorfabrik mit zusammensetzbaren Modifikatoren zum Erstellen eines Modifikators verwenden, können auch übergeordnete APIs wie animate*AsState
und andere APIs mit Compose-Status-gestützten Animationen verwendet werden. Das folgende Snippet zeigt beispielsweise einen Modifizierer, der bei Aktivierung/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 } }
return graphicsLayer { this.alpha = alpha }
Wenn der benutzerdefinierte Modifikator eine praktische Methode zur Bereitstellung von Standardwerten aus einem CompositionLocal
ist, lässt sich dies am einfachsten mit einer zusammensetzbaren Modifizierer-Factory implementieren:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Bei diesem Ansatz gibt es einige Einschränkungen, die im Folgenden erläutert werden.
CompositionLocal
-Werte werden auf der Aufrufseite der Modifikatorfabrik aufgelöst
Wenn Sie einen benutzerdefinierten Modifikator mit einer Factory für zusammensetzbare Modifikatoren erstellen, übernehmen Kompositionslokale den Wert aus der Zusammensetzungsstruktur, in der sie erstellt wurden, und nicht verwendet. Dies kann zu unerwarteten Ergebnissen führen. Nehmen wir z. B. das obige Beispiel für den lokalen Kompositionsmodifikator, 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 dies nicht die erwartete Funktionsweise des Modifizierers ist, verwenden Sie stattdessen einen benutzerdefinierten Modifier.Node
, da Kompositionslokale am Nutzungsort korrekt aufgelöst werden und sicher hochgezogen werden können.
Zusammensetzbare Funktionsmodifikatoren werden nie übersprungen
Zusammensetzbare Fabrikmodifikatoren werden niemals übersprungen, da zusammensetzbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass die Modifikatorfunktion bei jeder Neuzusammensetzung aufgerufen wird, was teuer werden kann, wenn sie häufig neu zusammensetzt.
Modifikatoren für zusammensetzbare Funktionen müssen innerhalb einer zusammensetzbaren Funktion aufgerufen werden
Wie bei allen zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifikator aus der Zusammensetzung aufgerufen werden. Dies schränkt ein, wohin ein Modifikator hochgezogen werden kann, da er niemals aus der Komposition hochgezogen werden kann. Im Gegensatz dazu können nicht zusammensetzbare Modifikatorfabriken aus zusammensetzbaren Funktionen entfernt 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 }
Verhalten von benutzerdefinierten Modifikatoren mit Modifier.Node
implementieren
Modifier.Node
ist eine Low-Level-API zum Erstellen von Modifikatoren in der Funktion „Compose“. Es ist die API, in der Compose seine eigenen Modifikatoren implementiert. Sie ist die leistungsstärkste Methode, benutzerdefinierte Modifikatoren zu erstellen.
Benutzerdefinierten Modifikator mit Modifier.Node
implementieren
Die Implementierung eines benutzerdefinierten Modifikators mit Modifier.Node umfasst drei Schritte:
- Eine
Modifier.Node
-Implementierung, die die Logik und den Status des Modifizierers enthält. - Ein
ModifierNodeElement
, das Modifikatorknoteninstanzen erstellt und aktualisiert. - Eine optionale Modifikatorfabrik, wie oben beschrieben.
ModifierNodeElement
-Klassen sind zustandslos und bei jeder Neuzusammensetzung werden neue Instanzen zugewiesen. Modifier.Node
-Klassen können hingegen zustandsorientiert sein, bleiben auch über mehrere Neuzusammensetzungen bestehen und können sogar wiederverwendet werden.
Im folgenden Abschnitt werden die einzelnen Teile beschrieben und ein Beispiel für das Erstellen eines benutzerdefinierten Modifikators zum Zeichnen eines Kreises gezeigt.
Modifier.Node
Mit der Implementierung Modifier.Node
(in diesem Beispiel CircleNode
) wird die Funktionalität des benutzerdefinierten Modifikators 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. Es gibt verschiedene Knotentypen, je nachdem, welche Funktionalität Ihr Modifikator erfordert. Das obige Beispiel muss in der Lage sein, zu zeichnen. Deshalb wird DrawModifierNode
implementiert, mit dem die „draw“-Methode überschrieben werden kann.
Folgende Typen sind verfügbar:
Knoten |
Verwendung |
Beispiellink |
Ein |
||
Ein |
||
Wenn diese Schnittstelle implementiert wird, kann der |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
|
||
Ein Dies kann nützlich sein, wenn Sie mehrere Knotenimplementierungen zu einer zusammenfassen möchten. |
||
Ermöglicht es |
Knoten werden automatisch entwertet, wenn eine Aktualisierung für ihr entsprechendes Element aufgerufen wird. Da es sich bei unserem Beispiel um DrawModifierNode
handelt, löst jedes Mal, wenn das Element aktualisiert wird, eine Neuzeichnung aus und die 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 enthält, die den benutzerdefinierten Modifikator erstellen oder aktualisieren sollen:
// 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, die den Modifikatorknoten instanziiert. Diese wird aufgerufen, um den Knoten zu erstellen, wenn der Modifikator zum ersten Mal angewendet wird. In der Regel umfasst dies die Erstellung des Knotens und dessen Konfiguration mit den Parametern, die an die Modifikator-Factory übergeben wurden.update
: Diese Funktion wird immer dann aufgerufen, wenn sich dieser Modifikator an derselben Stelle befindet, an der dieser Knoten bereits vorhanden ist, sich aber ein Attribut geändert hat. Dies wird durch die Methodeequals
der Klasse bestimmt. Der zuvor erstellte Modifikatorknoten wird als Parameter an den Aufrufupdate
gesendet. An dieser Stelle sollten Sie die Eigenschaften der Knoten so aktualisieren, dass sie den aktualisierten Parametern entsprechen. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerungen, dieModifier.Node
mit sich bringt. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt in der Methodeupdate
einen neuen 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 „ist“-Vergleich 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 das Element Eigenschaften hat, die nicht dazu beitragen, dass 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 Padding-Modifikatorelement.
Modifizierer Werkseinstellung
Dies ist die öffentliche API-Oberfläche Ihres Modifizierers. Bei den meisten Implementierungen wird einfach das Modifikatorelement erstellt und in die Modifikatorkette aufgenommen:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Vollständiges Beispiel
Diese drei Teile ergeben zusammen den benutzerdefinierten Modifikator, mit dem mit den Modifier.Node
APIs ein Kreis 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 Situationen bei Verwendung von Modifier.Node
Wenn Sie benutzerdefinierte Modifikatoren mit Modifier.Node
erstellen, können im Folgenden einige gängige Situationen auftreten.
Null Parameter
Hat der Modifikator keine Parameter, muss er nicht aktualisiert werden und muss außerdem keine Datenklasse sein. Hier sehen Sie ein Beispiel für die Implementierung eines Modifikators, bei dem ein fester Wert für das Padding 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) } } }
Verweise auf lokale Kompositionen
Modifier.Node
-Modifikatoren beobachten nicht automatisch Änderungen an Objekten für den Status „Compose“, z. B. CompositionLocal
. Der Vorteil von Modifier.Node
-Modifikatoren gegenüber Modifikatoren, die gerade mit einer zusammensetzbaren Factory erstellt wurden, besteht darin, dass sie mit currentValueOf
den Wert der lokalen Komposition aus dem Ort lesen können, an dem der Modifikator im UI-Baum verwendet wird, nicht dort, wo der Modifikator zugewiesen ist.
Instanzen von Modifikatorknoten beobachten jedoch Statusänderungen nicht automatisch. Wenn Sie automatisch auf eine Änderung der lokalen Zusammensetzung reagieren möchten, können Sie den aktuellen Wert innerhalb eines Bereichs lesen:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
undIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
In diesem Beispiel wird der Wert von LocalContentColor
beobachtet, um einen Hintergrund basierend auf seiner Farbe zu zeichnen. Da ContentDrawScope
Snapshot-Änderungen erkennt, wird dies automatisch neu erstellt, 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 Modifizierer automatisch aktualisieren möchten, verwenden Sie einen ObserverModifierNode
.
In Modifier.scrollable
wird dieses Verfahren beispielsweise verwendet, 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) } }
Animierender Modifikator
Modifier.Node
-Implementierungen haben Zugriff auf eine coroutineScope
. Dies ermöglicht die Verwendung der Compose Animatable APIs. Dieses Snippet ändert beispielsweise die oben angegebene CircleNode
so, dass sie 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 durch Delegierung zwischen Modifikatoren freigeben
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, aber es kann auch verwendet werden, um den gemeinsamen Status über alle Modifikatoren hinweg zu teilen.
Hier ein Beispiel für eine einfache Implementierung eines anklickbaren Modifikatorknotens mit gemeinsamen 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 die entsprechenden ModifierNodeElement
-Aufrufe aktualisiert werden. Bei komplexeren Modifikatoren 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 mit dem benutzerdefinierten Modifikator sowohl das Layout als auch die Zeichnung geändert werden. Wenn Sie die automatische Entwertung deaktivieren, können Sie eine Zeichnung nur dann ungültig machen, wenn nur zeichnenbezogene Eigenschaften wie color
geändert oder das Layout nicht ungültig wird.
Dies kann die Leistung des Modifizierers verbessern.
Unten sehen Sie ein hypothetisches Beispiel mit einem Modifikator, der die Lambda-Eigenschaften color
, size
und onClick
hat. Dieser Modifikator macht nur erforderliche Entwertungen und überspringt alle Entwertungen, die nicht:
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) } } }