Compose bietet viele Modifikatoren für gängige Verhaltensweisen. Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.
Modifikatoren haben mehrere Teile:
- Eine Modifikator-Factory
- Dies ist eine Erweiterungsfunktion von
Modifier
, die eine idiomatische API für Ihren Modifikator bietet und es ermöglicht, Modifikatoren einfach zu verketten. Die Modifikator-Factory erstellt die Modifikatorelemente, die von Compose zum Ändern der Benutzeroberfläche verwendet werden.
- Dies ist eine Erweiterungsfunktion von
- 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 Modifikator graphicsLayer
implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifikator-Factory bereit.
Bevor Sie einen 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 mit einer composable modifier factory 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 praktische Methode ist, Standardwerte aus einer CompositionLocal
anzugeben, ist die Verwendung einer kombinierbaren Modifikator-Factory die einfachste Implementierungsmethode:
@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 an der Aufrufstelle der Modifikator-Fabrik aufgelöst.
Wenn Sie einen benutzerdefinierten Modifikator mit einer composable modifier factory erstellen, nehmen Zusammensetzungs-Locals den Wert aus dem Kompositionbaum an, 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
Modifikatoren für kombinierbare Fabriken werden nie übersprungen, da kombinierbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifizierfunktion bei jeder Neuzusammensetzung aufgerufen wird. Das kann bei häufigen Neuzusammensetzungen kostspielig sein.
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 Vergleich dazu können nicht kombinierbare Modifikator-Fabriken aus kombinierbaren Funktionen ausgelagert 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 }
Benutzerdefiniertes Modifikatorverhalten 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. Sie ist die leistungsstärkste Methode zum Erstellen benutzerdefinierter Modifikatoren.
Benutzerdefinierten Modifikator mit Modifier.Node
implementieren
Die Implementierung eines benutzerdefinierten Modifiers mit Modifier.Node umfasst drei Teile:
- Eine
Modifier.Node
-Implementierung, die die Logik und den Status des Modifiers enthält. - Ein
ModifierNodeElement
, mit dem Modifikatorknoteninstanzen erstellt und aktualisiert werden. - 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 und ü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 Modusfunktion übergeben wird.
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 in unserem Beispiel um ein DrawModifierNode
handelt, wird jedes Mal, wenn das Element aktualisiert wird, ein Neuzeichnen ausgelöst und die Farbe wird korrekt aktualisiert. Sie können die automatische Invalidation 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 aufgerufen, wenn dieser Modifikator an derselben Stelle angegeben wird, an der dieser Knoten bereits vorhanden ist, sich aber eine Eigenschaft geändert hat. 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 so, dass sie den aktualisierten Parametern entsprechen. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerung, dieModifier.Node
bietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt mit der Methodeupdate
einen neuen 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 Vergleich mit dem vorherigen Element den Wert „falsch“ zurückgibt.
Im Beispiel oben wird dazu eine Datenklasse verwendet. Mit diesen Methoden wird geprüft, ob ein Knoten aktualisiert werden muss oder nicht. 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 Anwendungsfälle für 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 auch 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 beobachten nicht automatisch Änderungen an Compose-Statusobjekten wie CompositionLocal
. Der Vorteil von Modifier.Node
-Modifikator gegenüber Modifikatoren, die nur mit einer composable factory erstellt werden, besteht darin, dass der Wert der Komposition lokal an der Stelle gelesen werden kann, an der der Modifizierer im UI-Baum verwendet wird, nicht an der Stelle, an der er zugewiesen wird, mit currentValueOf
.
Modifizierknoteninstanzen beobachten jedoch nicht automatisch Statusänderungen. Wenn du automatisch auf eine lokale Änderung einer Komposition reagieren möchtest, kannst du den aktuellen Wert innerhalb eines Gültigkeitsbereichs lesen:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
undIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
In diesem Beispiel wird der Wert von LocalContentColor
verwendet, um einen Hintergrund basierend auf seiner Farbe zu zeichnen. Da ContentDrawScope
Snapshot-Änderungen beobachtet, wird das Diagramm 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 den Modifikator automatisch aktualisieren möchten, verwenden Sie eine ObserverModifierNode
.
Modifier.scrollable
verwendet diese Methode beispielsweise, um Änderungen an 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) } }
Animierter Modifikator
Modifier.Node
-Implementierungen haben Zugriff auf eine 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. Es gibt viele Anwendungsfälle dafür, z. B. das Extrahieren gemeinsamer Implementierungen für verschiedene Modifikatoren. Es kann aber auch verwendet werden, um einen gemeinsamen Status für verschiedene Modifikatoren zu teilen.
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 ungültig, wenn die entsprechenden ModifierNodeElement
-Aufrufe aktualisiert werden. Bei komplexeren Modifikatoren kann es jedoch sinnvoll sein, dieses Verhalten zu deaktivieren, um genauer festlegen zu können, wann Phasen durch den Modifikator ungültig werden.
Das kann besonders nützlich sein, wenn Ihr benutzerdefinierter Modifikator sowohl das Layout als auch das Zeichnen ä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) } } }