Compose bietet viele Modifikatoren für gängige Verhaltensweisen, Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.
Modifikatoren bestehen aus mehreren Teilen:
- Eine Modifikator-Factory
- Dies ist eine Erweiterungsfunktion für
Modifier, die eine idiomatische API für Ihren Modifikator bereitstellt und es ermöglicht, Modifikatoren miteinander zu verketten. Die Modifikator-Factory erzeugt die Modifikatorelemente, die von Compose verwendet werden, um die Benutzeroberfläche zu ändern.
- Dies ist eine Erweiterungsfunktion für
- Ein Modifikatorelement
- Hier können Sie das Verhalten Ihres Modifikators implementieren.
Je nach erforderlicher Funktionalität gibt es mehrere Möglichkeiten, einen benutzerdefinierten Modifikator zu implementieren. Oft ist es am einfachsten, eine benutzerdefinierte Modifikator-Factory zu implementieren, die andere bereits definierte Modifikator-Factorys kombiniert. Wenn Sie ein benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifikatorelement mit den Modifier.Node APIs, die zwar auf einer niedrigeren Ebene liegen, aber mehr Flexibilität bieten.
Vorhandene Modifikatoren miteinander verketten
Oft ist es möglich, benutzerdefinierte Modifikatoren mithilfe vorhandener Modifikatoren zu erstellen. Beispielsweise wird Modifier.clip() mit dem Modifikator graphicsLayer implementiert. Bei dieser Strategie werden vorhandene Modifikatorelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifikator-Factory bereit.
Bevor Sie einen eigenen benutzerdefinierten Modifikator implementieren, sollten Sie prüfen, ob Sie dieselbe Strategie verwenden können.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Wenn 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 mit einer zusammensetzbaren Modifikator-Factory erstellen
Sie können auch einen benutzerdefinierten Modifikator mit einer zusammensetzbaren Funktion erstellen, um Werte an einen vorhandenen Modifikator zu übergeben. Dies wird als Factory für zusammensetzbare Modifikatoren bezeichnet.
Wenn Sie einen Modifikator mit einer kombinierbaren Modifikator-Factory erstellen, können Sie auch Compose APIs auf höherer Ebene verwenden, z. B. animate*AsState und andere zustandsbasierte Animations-APIs von Compose. Das folgende Snippet zeigt beispielsweise einen Modifikator, der eine Alpha-Änderung animiert, wenn er aktiviert/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 Convenience-Methode ist, um Standardwerte aus einem CompositionLocal bereitzustellen, ist die einfachste Möglichkeit, dies zu implementieren, die Verwendung einer zusammensetzbaren Modifikator-Factory:
@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 Modifikator-Factory aufgelöst
Wenn Sie einen benutzerdefinierten Modifikator mit einer zusammensetzbaren Modifikator-Factory erstellen, übernehmen CompositionLocals den Wert aus dem Kompositionsbaum, in dem sie erstellt und nicht verwendet werden. Das kann zu unerwarteten Ergebnissen führen. Sehen wir uns das zuvor erwähnte Beispiel für den CompositionLocal-Modifikator 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 du nicht möchtest, dass dein Modifikator so funktioniert, verwende stattdessen ein benutzerdefiniertes Modifier.Node, da CompositionLocals am Verwendungsort korrekt aufgelöst und sicher hochgezogen werden können.
Modifikatoren für zusammensetzbare Funktionen werden nie übersprungen
Zusammensetzbare Factory-Modifikatoren werden nie übersprungen, da zusammensetzbare Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass die Modifikatorfunktion bei jeder Neuzusammensetzung aufgerufen wird. Das kann bei häufigen Neuzusammensetzungen sehr rechenintensiv sein.
Modifikatoren für zusammensetzbare Funktionen müssen innerhalb einer zusammensetzbaren Funktion aufgerufen werden
Wie alle zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifikator innerhalb der Komposition aufgerufen werden. Dadurch wird eingeschränkt, wohin ein Modifikator hochgezogen werden kann, da er nie aus der Komposition herausgezogen werden kann. Im Vergleich dazu können nicht zusammensetzbare Modifikator-Factories aus zusammensetzbaren Funktionen herausgehoben 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 Modifikatorverhalten mit Modifier.Node implementieren
Modifier.Node ist eine Low-Level-API zum Erstellen von Modifikatoren in Compose. Es ist dieselbe API, in der Compose seine eigenen Modifikatoren implementiert, und die leistungsstärkste Methode, um benutzerdefinierte Modifikatoren zu erstellen.
Benutzerdefinierten Modifikator mit Modifier.Node implementieren
Die Implementierung eines benutzerdefinierten Modifikators mit Modifier.Node besteht aus drei Teilen:
- Eine
Modifier.Node-Implementierung, die die Logik und den Status des Modifikators enthält. - Ein
ModifierNodeElement, der Modifikator-Knoteninstanzen erstellt und aktualisiert. - Eine optionale Modifikator-Factory, wie oben beschrieben.
ModifierNodeElement-Klassen sind zustandslos und bei jeder Neuzusammensetzung werden neue Instanzen zugewiesen. Modifier.Node-Klassen können dagegen zustandsorientiert sein und bleiben über mehrere Neuzusammensetzungen hinweg erhalten. Sie können sogar wiederverwendet werden.
Im folgenden Abschnitt werden die einzelnen Teile beschrieben und es wird ein Beispiel für das Erstellen eines benutzerdefinierten Modifikators zum Zeichnen eines Kreises gezeigt.
Modifier.Node
Die Modifier.Node-Implementierung (in diesem Beispiel CircleNode) implementiert die Funktionalität Ihres benutzerdefinierten Modifikators.
// 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. Im vorherigen Beispiel muss gezeichnet werden können. Daher wird DrawModifierNode implementiert, wodurch die Methode „draw“ überschrieben werden kann.
Folgende Typen sind verfügbar:
Knoten |
Verwendung |
Beispiellink |
Ein |
||
Ein |
||
Durch die Implementierung dieser Schnittstelle kann Ihr |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
|
||
Ein Dies kann nützlich sein, um mehrere Knotenimplementierungen zu einer zusammenzufassen. |
||
Ermöglicht es |
Knoten werden automatisch ungültig, wenn die Aktualisierung für das entsprechende Element aufgerufen wird. Da es sich in unserem Beispiel um ein DrawModifierNode handelt, wird bei jedem Aufruf von „update“ für das Element ein Neuzeichnen ausgelöst und die Farbe wird korrekt aktualisiert. Sie können die automatische Entwertung deaktivieren, wie im Abschnitt Automatische Entwertung von Knoten deaktivieren beschrieben.
ModifierNodeElement
Ein ModifierNodeElement ist eine unveränderliche Klasse, die die Daten zum Erstellen oder Aktualisieren Ihres benutzerdefinierten Modifikators 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 den Modifikatorknoten instanziiert. Diese wird aufgerufen, um den Knoten zu erstellen, wenn der Modifikator zum ersten Mal angewendet wird. In der Regel wird dazu der Knoten erstellt und mit den Parametern konfiguriert, die an die Modifier-Factory übergeben wurden.update: Diese Funktion wird aufgerufen, wenn dieser Modifikator an derselben Stelle bereitgestellt wird, an der dieser Knoten bereits vorhanden ist, sich aber eine Property geändert hat. Dies wird durch die Methodeequalsder Klasse bestimmt. Der zuvor erstellte Modifikatorknoten wird als Parameter an den Aufrufupdategesendet. An dieser Stelle sollten Sie die Attribute der Knoten so aktualisieren, dass sie den aktualisierten Parametern entsprechen. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerung, dieModifier.Nodebietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt einen neuen Knoten in der Methodeupdatezu 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 Gleichheitsvergleich mit dem vorherigen Element „false“ zurückgibt.
Im vorherigen Beispiel wird dazu eine Datenklasse verwendet. Mit diesen Methoden wird geprüft, ob ein Knoten aktualisiert werden muss oder nicht. Wenn Ihr Element Attribute hat, die nicht dazu beitragen, ob ein Knoten aktualisiert werden muss, oder wenn 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.
Modifier Factory
Dies ist die öffentliche API-Oberfläche Ihres Modifikators. In den meisten Implementierungen wird das Modifikatorelement erstellt und der Modifikatorkette hinzugefügt:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Vollständiges Beispiel
Diese drei Teile ergeben zusammen den benutzerdefinierten 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 mit Modifier.Node
Hier sind einige häufige Situationen, die beim Erstellen benutzerdefinierter Modifikatoren mit Modifier.Node auftreten können.
Keine Parameter
Wenn Ihr Modifikator keine Parameter hat, muss er nie aktualisiert werden und muss auch keine Datenklasse sein. Hier ist ein Beispiel für die Implementierung eines Modifikators, der ein festes Padding auf eine komponierbare Funktion anwendet:
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) } } }
Referenzkompositions-Locals
Modifier.Node-Modifikatoren beobachten Änderungen an Compose-Zustandsobjekten wie CompositionLocal nicht automatisch. Der Vorteil von Modifier.Node-Modifikatoren gegenüber Modifikatoren, die nur mit einer zusammensetzbaren Factory erstellt werden, besteht darin, dass sie den Wert des lokalen Elements der Komposition dort lesen können, wo der Modifikator in Ihrem UI-Baum verwendet wird, und nicht dort, wo der Modifikator mit currentValueOf zugewiesen wird.
Modifikatorknoteninstanzen beobachten Statusänderungen jedoch nicht automatisch. Wenn du automatisch auf eine Änderung einer CompositionLocal reagieren möchtest, kannst du den aktuellen Wert innerhalb eines Bereichs lesen:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScopeundIntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
In diesem Beispiel wird der Wert von LocalContentColor beobachtet, um einen Hintergrund basierend auf seiner Farbe zu zeichnen. Da ContentDrawScope Snapshot-Änderungen beobachtet, wird die Ansicht 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 Modifikator automatisch aktualisieren möchten, verwenden Sie einen ObserverModifierNode.
Beispiel: Modifier.scrollable verwendet diese Technik, um Änderungen in LocalDensity zu beobachten. 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) } }
Modifikator animieren
Modifier.Node-Implementierungen haben Zugriff auf ein coroutineScope. Dadurch können die Compose Animatable APIs verwendet werden. In diesem Snippet wird beispielsweise das zuvor gezeigte CircleNode so geändert, dass es 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) ) { } } } }
Zustand zwischen Modifikatoren mithilfe von Delegierung teilen
Modifier.Node-Modifikatoren können an andere Knoten delegieren. Es gibt viele Anwendungsfälle dafür, z. B. das Extrahieren gängiger Implementierungen über verschiedene Modifikatoren hinweg. Es kann aber auch verwendet werden, um einen gemeinsamen Status über Modifikatoren hinweg zu teilen.
Hier ein Beispiel für eine einfache Implementierung eines anklickbaren Modifikatorknotens, der Interaktionsdaten weitergibt:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Automatische Knotenentwertung deaktivieren
Modifier.Node-Knoten werden automatisch ungültig, wenn die entsprechenden ModifierNodeElement-Aufrufe aktualisiert werden. Bei komplexen Modifikatoren kann es sinnvoll sein, dieses Verhalten zu deaktivieren, um genauer zu steuern, wann der Modifikator Phasen ungültig macht.
Das ist besonders nützlich, wenn Ihr benutzerdefinierter Modifikator sowohl das Layout als auch das Zeichnen ändert. Wenn Sie die automatische Entwertung deaktivieren, können Sie die Zeichnung nur dann entwerten, wenn sich nur zeichnungsbezogene Attribute wie color ändern. Dadurch wird verhindert, dass das Layout ungültig wird, und die Leistung des Modifikators kann verbessert werden.
Ein hypothetisches Beispiel dafür ist im folgenden Beispiel mit einem Modifikator zu sehen, der ein color-, size- und onClick-Lambda als Attribute hat. Dieser Modifikator entwertet nur das, was erforderlich ist, und überspringt alle unnötigen Entwertungen:
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) } } }