Compose udostępnia wiele gotowych modyfikatorów do typowych zachowań, ale możesz też tworzyć własne modyfikatory.
Modyfikatory składają się z kilku części:
- Fabryka modyfikatorów
- Jest to funkcja rozszerzenia w
Modifier, która udostępnia idiomatyczny interfejs API dla modyfikatora i umożliwia łączenie modyfikatorów. Fabryka modyfikatorów tworzy elementy modyfikatora używane przez Compose do modyfikowania interfejsu.
- Jest to funkcja rozszerzenia w
- Element modyfikatora
- W tym miejscu możesz zaimplementować zachowanie modyfikatora.
Modyfikator niestandardowy można wdrożyć na kilka sposobów, w zależności od potrzebnej funkcji. Często najprostszym sposobem na wdrożenie modyfikatora niestandardowego jest wdrożenie niestandardowej fabryki modyfikatorów, która łączy inne, już zdefiniowane fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego zachowania, zaimplementuj element modyfikatora za pomocą interfejsów API Modifier.Node, które są na niższym poziomie, ale zapewniają większą elastyczność.
Łączenie istniejących modyfikatorów
Często można tworzyć modyfikatory niestandardowe, korzystając z istniejących modyfikatorów. Na przykład Modifier.clip() jest implementowany za pomocą modyfikatora graphicsLayer. Ta strategia wykorzystuje istniejące elementy modyfikatora, a Ty udostępniasz własną fabrykę modyfikatorów niestandardowych.
Zanim wdrożysz własny modyfikator niestandardowy, sprawdź, czy możesz użyć tej samej strategii.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Jeśli często powtarzasz tę samą grupę modyfikatorów, możesz je opakować we własny modyfikator:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Tworzenie modyfikatora niestandardowego za pomocą fabryki modyfikatorów funkcji kompozycyjnych
Możesz też utworzyć modyfikator niestandardowy za pomocą funkcji typu „composable”, aby przekazywać wartości do istniejącego modyfikatora. Jest to tzw. fabryka modyfikatorów funkcji kompozycyjnych.
Użycie fabryki modyfikatorów kompozycyjnych do utworzenia modyfikatora umożliwia też korzystanie z interfejsów API Compose wyższego poziomu, takich jak animate*AsState i inne interfejsy API animacji oparte na stanie Compose. Na przykład ten fragment kodu pokazuje modyfikator, który animuje zmianę kanału alfa po włączeniu lub wyłączeniu:
@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 } }
Jeśli modyfikator niestandardowy jest metodą ułatwiającą podawanie wartości domyślnych z CompositionLocal, najprostszym sposobem jego wdrożenia jest użycie fabryki modyfikatorów funkcji kompozycyjnej:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
To podejście ma pewne ograniczenia, które opisujemy w sekcjach poniżej.
Wartości CompositionLocal są rozwiązywane w miejscu wywołania fabryki modyfikatorów
Podczas tworzenia modyfikatora niestandardowego za pomocą fabryki modyfikatorów kompozycyjnych zmienne lokalne kompozycji przyjmują wartość z drzewa kompozycji, w którym są tworzone, a nie używane. Może to prowadzić do nieoczekiwanych wyników. Rozważmy na przykład wspomniany wcześniej przykład modyfikatora lokalnego kompozycji, który został zaimplementowany nieco inaczej przy użyciu funkcji typu „composable”:
@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) } } }
Jeśli nie chcesz, aby modyfikator działał w ten sposób, użyj niestandardowego Modifier.Node, ponieważ elementy lokalne kompozycji zostaną prawidłowo rozwiązane w miejscu użycia i można je bezpiecznie podnieść.
Modyfikatory funkcji typu „composable” nigdy nie są pomijane
Modyfikatory fabryki funkcji typu „composable” nigdy nie są pomijane, ponieważ nie można pomijać funkcji typu „composable”, które zwracają wartości. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdej rekompozycji, co może być kosztowne, jeśli rekompozycja występuje często.
Modyfikatory funkcji typu „composable” muszą być wywoływane w funkcji typu „composable”
Podobnie jak wszystkie funkcje typu „composable”, funkcja fabryczna modyfikatora typu „composable” musi być wywoływana z poziomu kompozycji. Ogranicza to miejsce, do którego można przenieść modyfikator, ponieważ nie można go przenieść poza kompozycję. Dla porównania, fabryki modyfikatorów, które nie są typu „composable”, można wyciągnąć z funkcji typu „composable”, aby ułatwić ich ponowne użycie i poprawić wydajność:
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 }
Zaimplementuj niestandardowe zachowanie modyfikatora za pomocą Modifier.Node
Modifier.Node to interfejs API niższego poziomu do tworzenia modyfikatorów w Compose. Jest to ten sam interfejs API, w którym Compose implementuje własne modyfikatory, i najbardziej wydajny sposób tworzenia modyfikatorów niestandardowych.
Wdrażanie modyfikatora niestandardowego za pomocą Modifier.Node
Wdrożenie modyfikatora niestandardowego za pomocą Modifier.Node składa się z 3 części:
- Implementacja
Modifier.Node, która zawiera logikę i stan modyfikatora. ModifierNodeElement, który tworzy i aktualizuje instancje węzłów modyfikatora.- Opcjonalna fabryka modyfikatorów, jak opisano wcześniej.
Klasy ModifierNodeElement są bezstanowe, a nowe instancje są przydzielane przy każdej rekompozycji, natomiast klasy Modifier.Node mogą być stanowe i przetrwają wiele rekompozycji, a nawet mogą być używane wielokrotnie.
W następnej sekcji opisujemy każdą część i pokazujemy przykład tworzenia niestandardowego modyfikatora do rysowania okręgu.
Modifier.Node
Implementacja Modifier.Node (w tym przykładzie CircleNode) implementuje funkcję modyfikatora niestandardowego.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
W tym przykładzie rysuje okrąg w kolorze przekazanym do funkcji modyfikatora.
Węzeł implementuje interfejs Modifier.Node oraz co najmniej 1 typ węzła. Dostępne są różne typy węzłów w zależności od funkcji, której wymaga modyfikator. Poprzedni przykład musi mieć możliwość rysowania, więc implementuje interfejs DrawModifierNode, który umożliwia zastąpienie metody rysowania.
Dostępne są te typy:
Węzeł |
Wykorzystanie |
Przykładowy link |
|
||
|
||
Zaimplementowanie tego interfejsu umożliwia |
||
|
||
|
||
|
||
|
||
|
||
|
||
Może to być przydatne do połączenia wielu implementacji węzłów w jedną. |
||
Umożliwia klasom |
Węzły są automatycznie unieważniane, gdy wywoływana jest aktualizacja odpowiedniego elementu. Nasz przykład to DrawModifierNode, więc za każdym razem, gdy wywoływana jest aktualizacja elementu, węzeł wyzwala ponowne rysowanie i kolor jest prawidłowo aktualizowany. Możesz zrezygnować z automatycznego unieważniania, jak opisano w sekcji Rezygnacja z automatycznego unieważniania węzłów.
ModifierNodeElement
ModifierNodeElement to niezmienna klasa, która zawiera dane do utworzenia lub zaktualizowania modyfikatora niestandardowego:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
Implementacje ModifierNodeElement muszą zastąpić te metody:
create: to funkcja, która tworzy instancję węzła modyfikatora. Ta funkcja jest wywoływana w celu utworzenia węzła, gdy modyfikator jest stosowany po raz pierwszy. Zwykle polega to na utworzeniu węzła i skonfigurowaniu go za pomocą parametrów przekazanych do fabryki modyfikatorów.update: ta funkcja jest wywoływana, gdy ten modyfikator jest podany w tym samym miejscu, w którym ten węzeł już istnieje, ale właściwość uległa zmianie. Określa to metodaequalsklasy. Węzeł modyfikatora, który został wcześniej utworzony, jest wysyłany jako parametr do wywołaniaupdate. W tym momencie należy zaktualizować właściwości węzłów, aby odpowiadały zaktualizowanym parametrom. Możliwość ponownego użycia węzłów w ten sposób jest kluczowa dla zwiększenia wydajności, które zapewniaModifier.Node. Dlatego musisz zaktualizować istniejący węzeł, zamiast tworzyć nowy w metodzieupdate. W naszym przykładzie z okręgiem kolor węzła zostanie zaktualizowany.
Dodatkowo implementacje ModifierNodeElement muszą też implementować equals i hashCode. Metoda update zostanie wywołana tylko wtedy, gdy porównanie z poprzednim elementem zwróci wartość false.
W poprzednim przykładzie użyto w tym celu klasy danych. Te metody służą do sprawdzania, czy węzeł wymaga aktualizacji. Jeśli element ma właściwości, które nie wpływają na to, czy węzeł musi zostać zaktualizowany, lub jeśli chcesz uniknąć klas danych ze względu na zgodność binarną, możesz ręcznie zaimplementować metody equals i hashCode, na przykład element modyfikatora dopełnienia.
Fabryka modyfikatorów
To jest publiczny interfejs API modyfikatora. Większość implementacji tworzy element modyfikatora i dodaje go do łańcucha modyfikatorów:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Kompletny przykład
Te 3 części tworzą niestandardowy modyfikator do rysowania okręgu za pomocą interfejsów API Modifier.Node:
// 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) } }
Typowe sytuacje, w których używa się Modifier.Node
Podczas tworzenia modyfikatorów niestandardowych za pomocą Modifier.Node możesz napotkać kilka typowych sytuacji.
Zero parametrów
Jeśli modyfikator nie ma parametrów, nigdy nie musi być aktualizowany i nie musi być klasą danych. Poniżej znajdziesz przykładową implementację modyfikatora, który stosuje stałą wartość dopełnienia do elementu kompozycyjnego:
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) } } }
Lokalne ustawienia kompozycji referencyjnej
Modyfikatory Modifier.Node nie obserwują automatycznie zmian w obiektach stanu Compose, takich jak CompositionLocal. Modyfikatory Modifier.Node mają przewagę nad modyfikatorami utworzonymi za pomocą funkcji kompozycyjnej, ponieważ mogą odczytywać wartość kompozycji lokalnej z miejsca, w którym modyfikator jest używany w drzewie interfejsu, a nie z miejsca, w którym jest przydzielony, za pomocą currentValueOf.
Jednak instancje węzłów modyfikatora nie obserwują automatycznie zmian stanu. Aby automatycznie reagować na zmianę zmiennej lokalnej kompozycji, możesz odczytać jej bieżącą wartość w zakresie:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScopeiIntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
W tym przykładzie wartość LocalContentColor jest obserwowana, aby narysować tło na podstawie koloru. Ponieważ ContentDrawScope obserwuje zmiany w zdjęciu,
automatycznie przerysowuje się, gdy zmienia się wartość LocalContentColor:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Aby reagować na zmiany stanu poza zakresem i automatycznie aktualizować modyfikator, użyj ObserverModifierNode.
Na przykład Modifier.scrollable używa tej techniki do obserwowania zmian w LocalDensity. Uproszczony przykład:
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) } }
Animowanie modyfikatora
Implementacje Modifier.Node mają dostęp do coroutineScope. Umożliwia to korzystanie z interfejsów API Compose Animatable. Na przykład ten fragment kodu modyfikuje pokazany wcześniej element CircleNode, aby wielokrotnie pojawiał się i znikał:
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) ) { } } } }
Udostępnianie stanu między modyfikatorami za pomocą przekazywania dostępu
Modyfikatory Modifier.Node mogą delegować zadania do innych węzłów. Istnieje wiele przypadków użycia, takich jak wyodrębnianie wspólnych implementacji w różnych modyfikatorach, ale można go też używać do udostępniania wspólnego stanu w modyfikatorach.
Oto przykład podstawowej implementacji węzła modyfikatora klikalnego, który udostępnia dane o interakcjach:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Rezygnacja z automatycznego unieważniania węzłów
Węzły Modifier.Node są automatycznie unieważniane, gdy aktualizowane są odpowiadające im wywołania ModifierNodeElement. W przypadku złożonych modyfikatorów możesz zrezygnować z tego zachowania, aby uzyskać bardziej szczegółową kontrolę nad tym, kiedy modyfikator unieważnia fazy.
Jest to szczególnie przydatne, jeśli modyfikator niestandardowy modyfikuje zarówno układ, jak i rysowanie. Rezygnacja z automatycznego unieważniania pozwala unieważnić rysowanie tylko wtedy, gdy zmieniają się właściwości związane z rysowaniem, takie jak color. Dzięki temu unikniesz unieważnienia układu i możesz zwiększyć wydajność modyfikatora.
Hipotetyczny przykład pokazano poniżej z modyfikatorem, który ma właściwości lambda color, size i onClick. Ten modyfikator unieważnia tylko to, co jest wymagane, pomijając niepotrzebne unieważnienia:
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) } } }