Compose bietet viele Modifizierer für gängige Verhaltensweisen, aber Sie können auch eigene benutzerdefinierte Modifizierer erstellen.
Modifizierer bestehen aus mehreren Teilen:
- Einer Modifizierer-Factory
- Dies ist eine Erweiterungsfunktion für
Modifier, die eine idiomatische API für Ihren Modifizierer bietet und es ermöglicht, Modifizierer zu verketten. Die Modifikator-Factory erzeugt die Modifikator-Elemente, die von Compose verwendet werden, um die Benutzeroberfläche zu ändern.
- Dies ist eine Erweiterungsfunktion für
- Ein Modifizierer-Element
- Hier können Sie das Verhalten Ihres Modifizierers implementieren.
Es gibt mehrere Möglichkeiten, einen benutzerdefinierten Modifizierer zu implementieren, je nach benötigter Funktionalität. Oft ist die einfachste Möglichkeit, einen benutzerdefinierten Modifikator zu implementieren, eine benutzerdefinierte Modifikator-Factory zu implementieren, die andere bereits definierte Modifikator-Factories kombiniert. Wenn Sie ein benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifizierer-Element mit den Modifier.Node-APIs, die auf einer niedrigeren Ebene angesiedelt sind, aber mehr Flexibilität bieten.
Vorhandene Modifizierer verketten
Oft ist es möglich, benutzerdefinierte Modifizierer mit vorhandenen Modifizierern zu erstellen. For
example, Modifier.clip() wird beispielsweise mit dem graphicsLayer
Modifizierer implementiert. Bei dieser Strategie werden vorhandene Modifizierer-Elemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifizierer-Factory bereit.
Bevor Sie Ihren eigenen benutzerdefinierten Modifizierer 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 Modifizierern häufig wiederholen, können Sie sie in einen eigenen Modifizierer einbinden:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Benutzerdefinierten Modifikator mit einer zusammensetzbaren Modifikator-Factory erstellen
Sie können auch einen benutzerdefinierten Modifizierer mit einer zusammensetzbaren Funktion erstellen, um Werte an einen vorhandenen Modifizierer zu übergeben. Dies wird als zusammensetzbare Modifizierer-Factory bezeichnet.
Wenn Sie eine zusammensetzbare Modifizierer-Factory verwenden, um einen Modifizierer zu erstellen, können Sie auch Compose-APIs auf höherer Ebene verwenden, z. B. animate*AsState und andere Compose Animations-APIs, die auf dem Status basieren. Das folgende Snippet zeigt beispielsweise einen Modifizierer, der eine Alpha-Änderung 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 praktische Methode ist, um Standardwerte aus einem CompositionLocal bereitzustellen, ist die einfachste Möglichkeit, dies zu implementieren, eine zusammensetzbare Modifikator-Factory zu verwenden:
@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 in den folgenden Abschnitten beschrieben werden.
CompositionLocal-Werte werden am Aufrufort der Modifizierer-Factory aufgelöst
Wenn Sie einen benutzerdefinierten Modifizierer mit einer zusammensetzbaren Modifizierer-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 Sie sich beispielsweise das zuvor erwähnte Beispiel für einen Modifizierer für Composition Locals an, das 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 Ihr Modifizierer nicht so funktionieren soll, verwenden Sie stattdessen einen benutzerdefinierten
Modifier.Node, da Composition Locals
am Verwendungsort korrekt aufgelöst und sicher nach oben verschoben werden können.
Modifizierer für zusammensetzbare Funktionen werden nie übersprungen
Modifizierer für zusammensetzbare Factories werden nie übersprungen, da zusammensetzbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifizierer-Funktion bei jeder Neukomposition aufgerufen wird, was teuer sein kann, wenn sie häufig neu zusammengesetzt wird.
Modifizierer für zusammensetzbare Funktionen müssen innerhalb einer zusammensetzbaren Funktion aufgerufen werden
Wie alle zusammensetzbaren Funktionen muss ein Modifizierer für zusammensetzbare Factories innerhalb der Komposition aufgerufen werden. Dadurch wird eingeschränkt, wohin ein Modifizierer verschoben werden kann, da er nie aus der Komposition verschoben werden kann. Im Vergleich dazu können nicht zusammensetzbare Modifizierer-Factories aus zusammensetzbaren Funktionen verschoben 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 Modifizierer-Verhalten mit Modifier.Node implementieren
Modifier.Node ist eine API auf niedrigerer Ebene zum Erstellen von Modifizierern in Compose. Es ist dieselbe API, mit der Compose seine eigenen Modifizierer implementiert, und die leistungsstärkste Möglichkeit, benutzerdefinierte Modifizierer zu erstellen.
Benutzerdefinierten Modifizierer mit Modifier.Node implementieren
Die Implementierung eines benutzerdefinierten Modifizierers mit Modifier.Node umfasst drei Teile:
- Eine
Modifier.Node-Implementierung, die die Logik und den Status Ihres Modifizierers enthält. - Ein
ModifierNodeElement, das Modifizierer Knoteninstanzen erstellt und aktualisiert. - Eine optionale Modifizierer-Factory, wie bereits beschrieben.
ModifierNodeElement -Klassen sind zustandslos und bei jeder Neukomposition werden neue Instanzen zugewiesen, während Modifier.Node-Klassen zustandsbehaftet sein können und mehrere Neukompositionen überdauern und sogar wiederverwendet werden können.
Im folgenden Abschnitt werden die einzelnen Teile beschrieben und ein Beispiel für das Erstellen eines benutzerdefinierten Modifizierers zum Zeichnen eines Kreises gezeigt.
Modifier.Node
Die Modifier.Node-Implementierung (in diesem Beispiel CircleNode) implementiert die Funktionalität Ihres benutzerdefinierten Modifizierers.
// 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 Modifizierer-Funktion übergeben wurde.
Ein Knoten implementiert Modifier.Node sowie null oder mehr Knotentypen. Es gibt verschiedene Knotentypen, je nach Funktionalität, die Ihr Modifizierer benötigt. Im vorherigen Beispiel muss gezeichnet werden können, daher wird DrawModifierNode implementiert, wodurch die Zeichenmethode ü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 |
||
|
||
Ein Das kann nützlich sein, um mehrere Knotenimplementierungen zu einer zusammenzufassen. |
||
Ermöglicht |
Knoten werden automatisch ungültig gemacht, wenn für das entsprechende Element „update“ aufgerufen wird. Da unser Beispiel ein DrawModifierNode ist, wird bei jedem Aufruf von „update“ für das Element ein erneutes Zeichnen ausgelöst und die Farbe des Knotens wird entsprechend aktualisiert. Es
ist möglich, die automatische Ungültigmachung zu deaktivieren, wie im
Abschnitt Automatische Ungültigmachung von Knoten deaktivieren beschrieben.
ModifierNodeElement
Ein ModifierNodeElement ist eine unveränderliche Klasse, die die Daten zum Erstellen oder Aktualisieren Ihres benutzerdefinierten Modifizierers 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, die Ihren Modifizierer-Knoten instanziiert. Sie wird aufgerufen, um den Knoten zu erstellen, wenn Ihr Modifizierer zum ersten Mal angewendet wird. In der Regel bedeutet das, den Knoten zu erstellen und mit den Parametern zu konfigurieren, die an die Modifizierer-Factory übergeben wurden.update: Diese Funktion wird aufgerufen, wenn dieser Modifizierer an derselben Stelle bereitgestellt wird, an der dieser Knoten bereits vorhanden ist, aber eine Property geändert wurde. Das wird durch dieequals-Methode der Klasse bestimmt. Der zuvor erstellte Modifizierer-Knoten wird als Parameter an denupdate-Aufruf gesendet. An dieser Stelle sollten Sie die Eigenschaften der Knoten entsprechend den aktualisierten Parametern aktualisieren. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist der Schlüssel zu den Leistungssteigerungen, dieModifier.Nodebietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt in derupdate-Methode einen neuen zu erstellen. In unserem Kreisbeispiel 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 mit „false“ zurückgegeben wird.
Im vorherigen Beispiel wird eine Datenklasse verwendet, um dies zu erreichen. 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 wenn Sie aus Gründen der binären Kompatibilität keine
Datenklassen verwenden möchten, können Sie
equals und hashCode manuell implementieren, z. B. das
Modifizierer-Element für den Abstand.
Modifizierer-Factory
Dies ist die öffentliche API-Oberfläche Ihres Modifizierers. Die meisten Implementierungen erstellen das Modifizierer-Element und fügen es der Modifizierer-Kette hinzu:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Vollständiges Beispiel
Diese drei Teile zusammen ergeben den benutzerdefinierten Modifizierer 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
Beim Erstellen benutzerdefinierter Modifizierer mit Modifier.Node können die folgenden Situationen auftreten.
Keine Parameter
Wenn Ihr Modifizierer keine Parameter hat, muss er nie aktualisiert werden und muss auch keine Datenklasse sein. Im Folgenden finden Sie eine Beispielimplementierung eines Modifizierers, der einem zusammensetzbaren Element 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) } } }
Auf Composition Locals verweisen
Modifier.Node -Modifizierer beobachten Änderungen an Compose-Statusobjekten wie CompositionLocal nicht automatisch. Der Vorteil von Modifier.Node Modifizierern gegenüber Modifizierern, die nur mit einer zusammensetzbaren Factory erstellt werden, besteht darin, dass sie den Wert des Composition Locals von der Stelle lesen können, an der der Modifizierer im UI-Baum verwendet wird, nicht von der Stelle, an der der Modifizierer zugewiesen wird, indem sie currentValueOf verwenden.
Modifizierer-Knoteninstanzen beobachten jedoch keine Statusänderungen automatisch. Wenn Sie automatisch auf eine Änderung des Composition Locals reagieren möchten, können Sie den aktuellen Wert in einem Bereich lesen:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
In diesem Beispiel wird der Wert von LocalContentColor beobachtet, um einen Hintergrund basierend auf der 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
Modifizierer automatisch aktualisieren möchten, verwenden Sie einen ObserverModifierNode.
Beispielsweise verwendet Modifier.scrollable diese Technik, um
Änderungen in LocalDensity zu beobachten. Ein vereinfachtes Beispiel finden Sie im folgenden 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) } }
Modifizierer animieren
Modifier.Node-Implementierungen haben Zugriff auf einen coroutineScope. Dadurch können die Compose Animatable APIs verwendet werden. In diesem Snippet wird beispielsweise der zuvor gezeigte CircleNode so geändert, dass er 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 mit Delegierung zwischen Modifizierern freigeben
Modifier.Node-Modifizierer können an andere Knoten delegieren. Dafür gibt es viele Anwendungsfälle, z. B. das Extrahieren gemeinsamer Implementierungen für verschiedene Modifizierer, aber es kann auch verwendet werden, um einen gemeinsamen Status für Modifizierer freizugeben.
Beispiel für eine grundlegende Implementierung eines klickbaren Modifizierer-Knotens, der Interaktionsdaten freigibt:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Automatische Ungültigmachung von Knoten deaktivieren
Modifier.Node -Knoten werden automatisch ungültig gemacht, wenn ihr entsprechendes ModifierNodeElement „update“ aufruft. Bei komplexen Modifizierern kann es sinnvoll sein, dieses Verhalten zu deaktivieren, um genauer zu steuern, wann Phasen ungültig gemacht werden.
Das ist besonders nützlich, wenn Ihr benutzerdefinierter Modifizierer sowohl das Layout als auch das Zeichnen ändert. Wenn Sie die automatische Ungültigmachung deaktivieren, können Sie das Zeichnen nur dann ungültig machen, wenn sich nur zeichnungsbezogene Eigenschaften wie color ändern. So wird die Ungültigmachung des Layouts vermieden und die Leistung Ihres Modifizierers kann verbessert werden.
Ein hypothetisches Beispiel dafür finden Sie im folgenden Beispiel mit einem Modifizierer, der eine color-Lambda-Funktion, eine size-Lambda-Funktion und eine onClick-Lambda-Funktion als Eigenschaften hat. Dieser Modifizierer macht nur das ungültig, was erforderlich ist, und überspringt alle unnötigen Ungültigmachungen:
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) } } }