Compose udostępnia wiele modyfikatorów do typowych zachowań, ale możesz też tworzyć własne modyfikatory niestandardowe.
Modyfikatory składają się z kilku części:
- Fabryka modyfikatorów
- Jest to funkcja rozszerzająca
Modifier, która udostępnia idiomatyczny interfejs API dla modyfikatora i umożliwia łączenie modyfikatorów w łańcuchy. Fabryka modyfikatorów tworzy elementy modyfikatorów używane przez Compose do modyfikowania interfejsu.
- Jest to funkcja rozszerzająca
- Element modyfikatora
- W tym miejscu możesz zaimplementować zachowanie modyfikatora.
W zależności od potrzebnej funkcjonalności możesz zaimplementować modyfikator niestandardowy na kilka sposobów. Najprostszym sposobem implementacji modyfikatora niestandardowego jest często zaimplementowanie 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 w łańcuchy
Modyfikatory niestandardowe można często tworzyć za pomocą istniejących modyfikatorów. Na
przykład, Modifier.clip() jest implementowany za pomocą graphicsLayer
modyfikatora. Ta strategia wykorzystuje istniejące elementy modyfikatorów, a Ty udostępniasz własną fabrykę modyfikatorów niestandardowych.
Zanim zaimplementujesz 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 typu „composable”
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 typu „composable”.
Użycie fabryki modyfikatorów typu „composable” do utworzenia modyfikatora umożliwia też korzystanie z
interfejsów API Compose wyższego poziomu, takich jak animate*AsState i inne Compose
interfejsy API animacji obsługiwane przez stan Compose. Na przykład ten fragment kodu pokazuje modyfikator, który animuje zmianę 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 wygodną metodą udostępniania wartości domyślnych z CompositionLocal, najłatwiej jest go zaimplementować za pomocą fabryki modyfikatorów typu „composable”:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
To podejście ma pewne wady, które zostały opisane w kolejnych sekcjach.
Wartości CompositionLocal są rozpoznawane w miejscu wywołania fabryki modyfikatorów
Podczas tworzenia modyfikatora niestandardowego za pomocą fabryki modyfikatorów typu „composable” lokalne kompozycje przyjmują wartość z drzewa kompozycji, w którym zostały utworzone, a nie z miejsca, w którym są używane. Może to prowadzić do nieoczekiwanych wyników. Rozważ na przykład wspomniany wcześniej przykład modyfikatora lokalnego kompozycji, który został zaimplementowany nieco inaczej za pomocą 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 zamiast tego niestandardowego
Modifier.Node, ponieważ lokalne kompozycje będą
prawidłowo rozpoznawane w miejscu użycia i można je bezpiecznie przenieść.
Modyfikatory funkcji typu „composable” nigdy nie są pomijane
Modyfikatory fabryki typu „composable” nigdy nie są pomijane, ponieważ nie można pominąć funkcji typu „composable” , które mają wartości zwracane. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdej ponownej kompozycji, co może być kosztowne, jeśli kompozycja jest często powtarzana.
Modyfikatory funkcji typu „composable” muszą być wywoływane w funkcji typu „composable”
Podobnie jak wszystkie funkcje typu „composable”, modyfikator fabryki typu „composable” musi być wywoływany w kompozycji. Ogranicza to miejsce, do którego można przenieść modyfikator, ponieważ nigdy nie można go przenieść poza kompozycję. Dla porównania, fabryki modyfikatorów niebędące funkcjami typu „composable” można przenieść poza funkcje typu „composable”, aby ułatwić ponowne użycie i zwiększyć 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 }
Implementowanie niestandardowego zachowania 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 jest to najbardziej wydajny sposób tworzenia modyfikatorów niestandardowych.
Implementowanie modyfikatora niestandardowego za pomocą Modifier.Node
Implementowanie modyfikatora niestandardowego za pomocą Modifier.Node składa się z 3 części:
- Implementacja
Modifier.Node, która zawiera logikę i stan modyfikatora. - A
ModifierNodeElement, który tworzy i aktualizuje instancje węzłów modyfikatorów. - Opcjonalna fabryka modyfikatorów, jak opisano wcześniej.
Klasy ModifierNodeElement są bezstanowe, a nowe instancje są przydzielane przy każdej ponownej kompozycji, natomiast klasy Modifier.Node mogą być stanowe i przetrwają wiele ponownych kompozycji, a nawet mogą być ponownie używane.
W sekcji poniżej opisujemy każdą część i pokazujemy przykład tworzenia modyfikatora niestandardowego do rysowania okręgu.
Modifier.Node
Implementacja Modifier.Node (w tym przykładzie CircleNode) implementuje funkcjonalność 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 Modifier.Node oraz 0 lub więcej typów węzłów. Istnieją różne typy węzłów w zależności od funkcjonalności wymaganej przez modyfikator. Poprzedni przykład musi być w stanie rysować, dlatego implementuje DrawModifierNode, co pozwala mu zastąpić metodę rysowania.
Dostępne typy:
Węzeł |
Wykorzystanie |
Przykładowy link |
|
||
|
||
Implementowanie tego interfejsu umożliwia |
||
|
||
|
||
|
||
|
||
|
||
|
||
Może to być przydatne do łączenia kilku implementacji węzłów w jedną. |
||
Umożliwia klasom |
Węzły są automatycznie unieważniane, gdy wywoływana jest aktualizacja odpowiadającego im elementu. Ponieważ nasz przykład to DrawModifierNode, za każdym razem, gdy wywoływana jest aktualizacja elementu, węzeł wywołuje ponowne rysowanie, a jego kolor jest prawidłowo aktualizowany. Można 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ępować te metody:
create: jest to funkcja, która tworzy instancję węzła modyfikatora. Jest ona wywoływana w celu utworzenia węzła, gdy modyfikator zostanie zastosowany po raz pierwszy. Zazwyczaj sprowadza się to do skonstruowania węzła i skonfigurowania go za pomocą parametrów przekazanych do fabryki modyfikatorów.update: ta funkcja jest wywoływana, gdy ten modyfikator jest udostępniany w tym samym miejscu, w którym już istnieje ten węzeł, ale zmieniła się właściwość. Jest to określane przez metodęequalsklasy. Węzeł modyfikatora, który został utworzony wcześniej, 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 w metodzieupdatemusisz zaktualizować istniejący węzeł, a nie tworzyć nowego. W naszym przykładzie okręgu aktualizowany jest kolor węzła.
Implementacje ModifierNodeElement muszą też implementować equals i hashCode. update zostanie wywołane tylko wtedy, gdy porównanie równości z poprzednim elementem zwróci wartość false.
W poprzednim przykładzie użyto 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ł wymaga aktualizacji, lub jeśli chcesz uniknąć klas danych ze względu na zgodność binarną, możesz ręcznie zaimplementować equals i hashCode, np. element modyfikatora dopełnienia .
Fabryka modyfikatorów
Jest to 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 razem tworzą modyfikator niestandardowy 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ć te typowe sytuacje.
Brak parametrów
Jeśli modyfikator nie ma parametrów, nigdy nie musi się aktualizować, a ponadto nie musi być klasą danych. Oto przykładowa implementacja modyfikatora, który stosuje stałą ilość dopełnienia do elementu typu „composable”:
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) } } }
Odwoływanie się do lokalnych kompozycji
Modyfikatory Modifier.Node nie obserwują automatycznie zmian w obiektach stanu Compose, takich jak CompositionLocal. Zaletą modyfikatorów Modifier.Node w porównaniu z modyfikatorami tworzonymi za pomocą fabryki typu „composable” jest to, że mogą one odczytywać wartość lokalnej kompozycji z miejsca, w którym modyfikator jest używany w drzewie interfejsu, a nie z miejsca, w którym jest przydzielany, za pomocą currentValueOf.
Instancje węzłów modyfikatorów nie obserwują jednak automatycznie zmian stanu. Aby automatycznie reagować na zmianę lokalnej kompozycji, możesz odczytać jej bieżącą wartość w zakresie:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
Ten przykład obserwuje wartość LocalContentColor, aby narysować tło na podstawie jego koloru. Ponieważ ContentDrawScope obserwuje zmiany migawek, automatycznie rysuje ponownie, gdy zmieni 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ć swój
modyfikator, użyj ObserverModifierNode.
Na przykład Modifier.scrollable używa tej metody do
obserwowania zmian w LocalDensity. Uproszczony przykład znajdziesz w tym przykładzie:
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 Animatable Compose. Na przykład ten fragment kodu modyfikuje pokazany wcześniej CircleNode, aby wielokrotnie się pojawiał 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ą delegowania
Modyfikatory Modifier.Node mogą delegować do innych węzłów. Istnieje wiele przypadków użycia tej funkcji, np. wyodrębnianie wspólnych implementacji w różnych modyfikatorach, ale można jej też używać do udostępniania wspólnego stanu między modyfikatorami.
Na przykład podstawowa implementacja węzła modyfikatora, który można kliknąć i który udostępnia dane interakcji:
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 odpowiadający im ModifierNodeElement wywołuje aktualizację. W przypadku złożonych modyfikatorów możesz zrezygnować z tego zachowania, aby uzyskać większą 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 umożliwia unieważnienie rysowania tylko wtedy, gdy zmienią się właściwości związane z rysowaniem, takie jak color. Pozwala to uniknąć unieważniania układu i może zwiększyć wydajność modyfikatora.
Hipotetyczny przykład tego rozwiązania znajdziesz w tym przykładzie z modyfikatorem, który ma właściwości color, size i lambdę onClick. Ten modyfikator unieważnia tylko to, co jest wymagane, pomijając niepotrzebne unieważnianie:
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) } } }