Compose bietet viele Modifikatoren für gängige Verhaltensweisen, die sofort einsatzbereit sind. Sie können aber auch eigene benutzerdefinierte Modifikatoren erstellen.
Modifikatoren bestehen aus mehreren Teilen:
- Eine Modifier-Factory
- Dies ist eine Erweiterungsfunktion für
Modifier, die eine idiomatische API für Ihren Modifier bietet und es ermöglicht, Modifier 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 benötigter 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 Modifikatorfactory implementieren, die andere bereits definierte Modifikatorfactories kombiniert. Wenn Sie ein benutzerdefiniertes Verhalten benötigen, implementieren Sie das Modifiziererelement mit den Modifier.Node-APIs. Diese sind zwar auf niedrigerer Ebene, bieten aber mehr Flexibilität.
Vorhandene Modifikatoren verketten
Häufig lassen sich benutzerdefinierte Modifikatoren erstellen, indem Sie vorhandene Modifikatoren verwenden. Modifier.clip() wird beispielsweise mit dem Modifikator graphicsLayer implementiert. Bei dieser Strategie werden vorhandene Modifiziererelemente verwendet und Sie stellen Ihre eigene benutzerdefinierte Modifiziererfactory 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 häufig dieselbe Gruppe von Modifikatoren verwenden, können Sie sie in einem eigenen Modifikator zusammenfassen:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Benutzerdefinierten Modifier mit einer zusammensetzbaren Modifier-Factory erstellen
Sie können auch einen benutzerdefinierten Modifier mit einer zusammensetzbaren Funktion erstellen, um Werte an einen vorhandenen Modifier zu übergeben. Dies wird als zusammensetzbare Modifikator-Factory bezeichnet.
Wenn Sie einen Modifier mit einer Factory für kombinierbare Modifier 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 Modifikator, der eine Änderung des Alphawerts 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 Modifier eine Hilfsmethode zum Bereitstellen von Standardwerten aus einem CompositionLocal ist, ist die einfachste Möglichkeit, dies zu implementieren, die Verwendung einer zusammensetzbaren Modifier-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 die lokalen Kompositionen 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 wir uns das oben erwähnte Beispiel für den lokalen Modifier für die Komposition an, das etwas anders mit einer zusammensetzbaren Funktion implementiert wird:
@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 das nicht Ihren Erwartungen entspricht, verwenden Sie stattdessen eine benutzerdefinierte Modifier.Node, da Kompositions-Locals am Verwendungsort richtig aufgelöst und sicher verschoben werden können.
Modifikatoren für komponierbare Funktionen werden nie übersprungen
Composable-Factory-Modifier werden nie übersprungen, da Composable-Funktionen mit Rückgabewerten nicht übersprungen werden können. Das bedeutet, dass Ihre Modifier-Funktion bei jeder Neuzusammenstellung aufgerufen wird. Das kann teuer sein, wenn sie häufig neu zusammengestellt wird.
Composable-Funktionsmodifizierer müssen innerhalb einer Composable-Funktion aufgerufen werden.
Wie alle zusammensetzbaren Funktionen muss ein zusammensetzbarer Factory-Modifier innerhalb der Komposition aufgerufen werden. Dadurch wird eingeschränkt, wohin ein Modifier verschoben werden kann, da er nie aus der Komposition verschoben werden kann. Im Vergleich dazu können nicht komponierbare Modifikator-Factories aus komponierbaren Funktionen herausgezogen 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 Modifier-Verhalten mit Modifier.Node implementieren
Modifier.Node ist eine API auf niedrigerer Ebene zum Erstellen von Modifikatoren in Compose. Es ist dieselbe API, in der Compose eigene Modifikatoren implementiert, und die leistungsstärkste Methode zum Erstellen benutzerdefinierter Modifikatoren.
Benutzerdefinierten Modifikator mit Modifier.Node implementieren
Die Implementierung eines benutzerdefinierten Modifikators mit Modifier.Node umfasst drei Teile:
- Eine
Modifier.Node-Implementierung, die die Logik und den Status des Modifiers enthält. - Ein
ModifierNodeElement, das Instanzen von Modifikator-Knoten erstellt und aktualisiert. - Eine optionale Modifier-Factory, wie zuvor beschrieben.
ModifierNodeElement-Klassen sind zustandslos und bei jeder Neukomposition werden neue Instanzen zugewiesen. Modifier.Node-Klassen können dagegen zustandsbehaftet sein und über mehrere Neukompositionen hinweg bestehen bleiben und sogar wiederverwendet werden.
Im folgenden Abschnitt wird jeder Teil 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. Je nach der erforderlichen Funktionalität des Modifikators gibt es verschiedene Knotentypen. Im vorherigen Beispiel muss gezeichnet werden 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 |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
Ein |
||
|
||
Ein Das kann nützlich sein, um mehrere Knotenimplementierungen in einer zusammenzufassen. |
||
Ermöglicht es |
Knoten werden automatisch ungültig, wenn die Aktualisierung für das entsprechende Element aufgerufen wird. Da unser Beispiel ein DrawModifierNode ist, wird bei jedem Aufruf von „update“ für das Element ein Neuzeichnen des Knotens ausgelöst und die Farbe wird korrekt aktualisiert. Sie können die automatische Ungültigmachung von Knoten deaktivieren. Weitere Informationen finden Sie im Abschnitt Automatische Ungültigmachung von Knoten deaktivieren.
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, mit der der Modifikator-Knoten instanziiert wird. Diese Funktion wird aufgerufen, um den Knoten zu erstellen, wenn der Modifier zum ersten Mal angewendet wird. Normalerweise besteht das darin, den Knoten zu erstellen und mit den Parametern zu konfigurieren, die an die Modifizierer-Factory übergeben wurden.update: Diese Funktion wird aufgerufen, wenn dieser Modifier 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 Änderungsknoten wird als Parameter an denupdate-Aufruf gesendet. An diesem Punkt sollten Sie die Eigenschaften der Knoten entsprechend den aktualisierten Parametern aktualisieren. Die Möglichkeit, Knoten auf diese Weise wiederzuverwenden, ist entscheidend für die Leistungssteigerungen, dieModifier.Nodebietet. Daher müssen Sie den vorhandenen Knoten aktualisieren, anstatt einen neuen in derupdate-Methode 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 vorherigen Beispiel 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 wenn Sie aus Gründen der binären Kompatibilität Datenklassen vermeiden möchten, können Sie equals und hashCode manuell implementieren, z. B. das Padding-Modifiziererelement.
Modifier Factory
Dies ist die öffentliche API-Oberfläche Ihres Modifikators. Bei den meisten Implementierungen wird das Modifiziererelement erstellt und der Modifiziererkette hinzugefügt:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Vollständiges Beispiel
Diese drei Teile bilden zusammen den benutzerdefinierten Modifier 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
Hier sind einige häufige Situationen, die beim Erstellen benutzerdefinierter Modifikatoren mit Modifier.Node auftreten können.
Keine Parameter
Wenn Ihr Modifier keine Parameter hat, muss er nie aktualisiert werden und muss auch keine Datenklasse sein. Im Folgenden finden Sie ein Beispiel für die Implementierung eines Modifiers, der einem Composable 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) } } }
Lokale Kompositionsreferenzen
Modifier.Node-Modifikatoren berücksichtigen Änderungen an Compose-Statusobjekten 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 der lokalen Komposition dort lesen können, wo der Modifikator in Ihrem UI-Baum verwendet wird, und nicht dort, wo der Modifikator mit currentValueOf zugewiesen wird.
Instanzen von Modifikator-Knoten reagieren jedoch nicht automatisch auf Zustandsänderungen. Wenn Sie automatisch auf eine Änderung des lokalen Werts einer Komposition 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 seiner Farbe zu zeichnen. Da ContentDrawScope Snapshot-Änderungen berücksichtigt, wird die Zeichnung automatisch neu erstellt, 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 Modifier automatisch aktualisieren möchten, verwenden Sie ein ObserverModifierNode.
Bei Modifier.scrollable wird diese Methode beispielsweise verwendet, um Änderungen bei LocalDensity zu beobachten. Ein vereinfachtes Beispiel ist unten zu sehen:
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 Beispiel wird 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) ) { } } } }
Status zwischen Modifikatoren mithilfe der Delegierung teilen
Modifier.Node-Modifikatoren können an andere Knoten delegieren. Dafür gibt es viele Anwendungsfälle, z. B. das Extrahieren gemeinsamer Implementierungen für verschiedene Modifikatoren. Es kann aber auch verwendet werden, um einen gemeinsamen Status für Modifikatoren freizugeben.
Hier ein Beispiel für eine einfache Implementierung eines klickbaren Modifier-Knotens, der Interaktionsdaten weitergibt:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Automatische Knotenungültigkeit deaktivieren
Modifier.Node-Knoten werden automatisch ungültig, wenn die entsprechenden ModifierNodeElement-Aufrufe aktualisiert werden. Bei komplexen Modifikatoren sollten Sie dieses Verhalten möglicherweise deaktivieren, um genauer zu steuern, wann Phasen durch den Modifikator ungültig werden.
Das ist besonders nützlich, wenn Ihr benutzerdefinierter Modifier sowohl das Layout als auch das Zeichnen ändert. Wenn Sie die automatische Ungültigmachung deaktivieren, wird die Zeichnung nur ungültig gemacht, wenn sich zeichnungsbezogene Eigenschaften wie color ändern. So wird das Layout nicht ungültig und die Leistung des Modifiers kann verbessert werden.
Ein hypothetisches Beispiel dafür ist im folgenden Beispiel mit einem Modifikator zu sehen, der die Lambdas color, size und onClick als Eigenschaften hat. Mit diesem Modifikator wird nur das ungültig gemacht, was erforderlich ist. Nicht benötigte Invalidierungen werden ü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) } } }