Compose bietet viele Modifikatoren für gängige Verhaltensweisen, Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.
Modifikatoren haben mehrere Teile:
- Eine Modifikatorfabrik
- Dies ist eine Erweiterungsfunktion für
Modifier
. Sie bietet eine idiomatische API für den Modifikator und ermöglicht die einfache Verkettung von Modifikatoren. Die Modifikator-Factory erzeugt die Modifikatorelemente, die von Compose zum Ändern der UI verwendet werden.
- Dies ist eine Erweiterungsfunktion für
- 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 graphicsLayer
-Modifikator implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifikator-Factory bereit.
Bevor Sie einen 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 einschließen:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Benutzerdefinierten Modifikator mithilfe einer Factory mit zusammensetzbaren Modifikatoren 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 bequeme Methode zum Bereitstellen von Standardwerten aus einem CompositionLocal
ist, lässt sich dies am einfachsten mit einer zusammensetzbaren Modifikator-Factory implementieren:
@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 auf der Aufrufseite der Modifikatorfabrik aufgelöst
Wenn Sie einen benutzerdefinierten Modifikator mit einer composable modifier factory erstellen, übernehmen Zusammensetzungs-Locals den Wert aus dem Kompositionbaum, 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
Zusammensetzbare Factory-Modifikatoren werden niemals übersprungen, da zusammensetzbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifikatorfunktion bei jeder Neuzusammensetzung aufgerufen wird, was teuer sein kann, wenn sie häufig neu zusammengesetzt wird.
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 Gegensatz dazu können nicht zusammensetzbare Modifikatorfabriken aus zusammensetzbaren Funktionen herausgezogen werden, um die Wiederverwendung zu vereinfachen 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. Dies ist dieselbe API, in der Compose seine eigenen Modifikatoren implementiert, und die leistungsstärkste Methode zum Erstellen benutzerdefinierter Modifikatoren.
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 den Status des Modifiers enthält. - Einem
ModifierNodeElement
, das Knoteninstanzen mit Modifikator erstellt und aktualisiert. - 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, ü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 Modifikatorfunktion übergeben wurde.
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 |
Ein |
||
Ein |
||
Wenn du diese Schnittstelle implementierst, kann deine |
||
Ein |
||
Eine |
||
Ein |
||
Ein |
||
Ein |
||
|
||
Eine Das kann nützlich sein, um mehrere Knotenimplementierungen zu einer zusammenzuführen. |
||
Ermöglicht es |
Knoten werden automatisch ungültig, wenn für das entsprechende Element die Funktion „update“ aufgerufen wird. Da es sich bei unserem Beispiel um eine DrawModifierNode
handelt, löst der Knoten bei jedem Aufruf einer Aktualisierung für das Element eine Neuzeichnung aus und seine Farbe wird korrekt aktualisiert. Sie können die automatische Entwertung 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:
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.update
: Diese Funktion wird immer dann aufgerufen, wenn dieser Modifikator 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 Modifier-Knoten wird als Parameter an denupdate
-Aufruf gesendet. Aktualisieren Sie nun die Knoteneigenschaften entsprechend den aktualisierten Parametern. Die Möglichkeit, Knoten auf diese Weise wiederverwendet zu werden, ist entscheidend für die Leistungssteigerungen, dieModifier.Node
mit sich bringen kann. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt einen neuen Knoten in der Methodeupdate
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 Beispiel oben 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 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 Situationen bei der Verwendung von 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 außerdem 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 berücksichtigen nicht automatisch Änderungen an Objekten des Erstellungsstatus (z. B. CompositionLocal
). Der Vorteil von Modifier.Node
Modifikatoren gegenüber Modifikatoren, die gerade mit einer zusammensetzbaren Factory erstellt wurden, besteht darin, dass sie den Wert der Komposition lokal auslesen können, wo der Modifikator in Ihrem UI-Baum verwendet wird, und nicht, wo der Modifikator zugewiesen ist. Dazu verwenden sie currentValueOf
.
Instanzen mit Modifikatorknoten berücksichtigen jedoch Statusänderungen nicht automatisch. Wenn Sie automatisch auf eine lokale Änderung der 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 anhand seiner Farbe zu zeichnen. Da ContentDrawScope
Snapshot-Änderungen beobachtet, wird dies 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 Gültigkeitsbereichs reagieren und den Modifikator automatisch aktualisieren möchten, verwenden Sie einen ObserverModifierNode
.
Modifier.scrollable
verwendet diese Methode beispielsweise, um Änderungen an 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) } }
Animierter Modifikator
Implementierungen von Modifier.Node
haben Zugriff auf ein 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. Dafür gibt es viele Anwendungsfälle, z. B. das Extrahieren gängiger Implementierungen in verschiedenen Modifikatoren. Es kann aber auch verwendet werden, um einen gemeinsamen Status für alle Modifikatoren zu erhalten.
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 entwertet, wenn die entsprechenden ModifierNodeElement
-Aufrufe aktualisiert werden. Bei einem komplexeren Modifikator 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 der benutzerdefinierte Modifikator sowohl das Layout als auch die Zeichnung ä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) } } }