Compose bietet viele Modifikatoren für gängige Verhaltensweisen, die Sie direkt verwenden können. Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.
Modifikatoren bestehen aus mehreren Teilen:
- 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 erzeugt die Modifikatorelemente, die von Compose zum Ändern der UI verwendet werden.
- Dies ist eine Erweiterungsfunktion von
- Ein Modifikatorelement
- Hier können Sie das Verhalten des Modifiers implementieren.
Je nach erforderlicher Funktionalität gibt es mehrere Möglichkeiten, einen benutzerdefinierten Modifizierer zu implementieren. Häufig besteht die einfachste Möglichkeit zur Implementierung eines benutzerdefinierten Modifikators darin, eine benutzerdefinierte Modifikator-Factory zu implementieren, die andere bereits definierte Modifikator-Factorys miteinander kombiniert. Wenn Sie mehr benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifikatorelement mithilfe der Modifier.Node
APIs. Diese sind eine niedrigere Ebene, 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 kombinierbaren Modifikator-Fabrik 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, übernehmen Zusammensetzungs-Locals den Wert aus dem Kompositionbaum, in dem sie erstellt, nicht verwendet werden. Dies 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.
Zusammensetzbare Funktionsmodifikatoren 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. 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.
Der folgende Abschnitt beschreibt die einzelnen Teile und zeigt ein Beispiel für das Erstellen eines benutzerdefinierten Modifikators zum Zeichnen eines Kreises.
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 |
||
Durch die Implementierung dieser Schnittstelle kann dein |
||
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 Ungültigkeitserklärung 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. Normalerweise führt dies dazu, dass der Knoten erstellt und mit den Parametern konfiguriert wird, 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. An dieser Stelle sollten Sie die Attribute der Knoten entsprechend den aktualisierten Parametern aktualisieren. 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 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 Kompositionen 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 Bild 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. 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.
Eine einfache Implementierung eines anklickbaren Modifikatorknotens, 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 manchmal 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 kann die Leistung des Modifizierers verbessert werden.
Ein hypothetisches Beispiel dafür finden Sie unten mit einem Modifikator mit den Lambda-Attributen color
, size
und onClick
. 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) } } }