Compose udostępnia wiele modyfikatorów do typowych działań, 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 modyfikatora i umożliwia łatwe łączenie modyfikatorów. Fabryka modyfikatorów tworzy elementy modyfikatorów używane przez Compose do modyfikowania interfejsu.
- Jest to funkcja rozszerzająca
- Element modyfikujący
- W tym miejscu możesz wdrożyć działanie modyfikatora.
W zależności od potrzebnej funkcjonalności modyfikator niestandardowy można wdrożyć na kilka sposobów. Często najłatwiejszym sposobem wdrożenia niestandardowego modyfikatora jest wdrożenie niestandardowej fabryki modyfikatorów, która łączy inne zdefiniowane już fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego działania, 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 dotychczasowych modyfikatorów. Na przykład Modifier.clip()
jest implementowany za pomocą modyfikatora graphicsLayer
. Ta strategia korzysta z istniejących elementów modyfikatora, a Ty dostarczasz własną fabrykę modyfikatorów niestandardowych.
Zanim wdrożysz własny modyfikator niestandardowy, sprawdź, czy możesz zastosować tę samą strategię.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Jeśli często powtarzasz tę samą grupę modyfikatorów, możesz ją zawinąć we własny modyfikator:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Tworzenie niestandardowego modyfikatora za pomocą fabryki modyfikatorów kompozycyjnych
Możesz też utworzyć niestandardowy modyfikator za pomocą funkcji kompozycyjnej, aby przekazywać wartości do istniejącego modyfikatora. Jest to tzw. fabryka modyfikatorów 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 Compose oparte na stanie. Na przykład poniższy fragment kodu pokazuje modyfikator, który animuje zmianę przezroczystości 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ą dostarczania wartości domyślnych z CompositionLocal
, najłatwiej jest go wdrożyć za pomocą fabryki modyfikatorów kompozycyjnych:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Takie podejście ma jednak pewne ograniczenia, które opisujemy poniżej.
Wartości CompositionLocal
są rozwiązywane w miejscu wywołania fabryki modyfikatorów.
Podczas tworzenia niestandardowego modyfikatora za pomocą fabryki modyfikatorów kompozycyjnych lokalne kompozycje przyjmują wartość z drzewa kompozycji, w którym zostały utworzone, a nie z drzewa, w którym są używane. Może to prowadzić do nieoczekiwanych wyników. Weźmy na przykład modyfikator composition local z przykładu powyżej, zaimplementowany nieco inaczej za pomocą funkcji 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ż kompozycje lokalne zostaną prawidłowo rozwiązane w miejscu użycia i można je bezpiecznie przenieść.
Modyfikatory funkcji typu „composable” nigdy nie są pomijane
Modyfikatory fabryki kompozycyjnej nigdy nie są pomijane, ponieważ funkcji kompozycyjnych, które zwracają wartości, nie można pominąć. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdej ponownej kompozycji, co może być kosztowne, jeśli ponowna kompozycja występuje często.
Modyfikatory funkcji typu „composable” muszą być wywoływane w funkcji typu „composable”
Podobnie jak wszystkie funkcje kompozycyjne, modyfikator fabryki funkcji kompozycyjnej musi być wywoływany w ramach kompozycji. Ogranicza to miejsce, do którego można przenieść modyfikator, ponieważ nigdy nie można go przenieść poza kompozycję. W porównaniu z tym fabryki modyfikatorów, które nie są kompozycyjne, można wyodrębnić z funkcji kompozycyjnych, aby ułatwić ich 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 działania 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. Jest to też najbardziej wydajny sposób tworzenia modyfikatorów niestandardowych.
Wdrażanie niestandardowego modyfikatora za pomocą Modifier.Node
Implementacja niestandardowego modyfikatora 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ła modyfikatora.- Opcjonalna fabryka modyfikatorów, jak opisano powyżej.
Klasy ModifierNodeElement
nie mają stanu, a nowe instancje są przydzielane przy każdej ponownej kompozycji, natomiast klasy Modifier.Node
mogą mieć stan i przetrwać wiele ponownych kompozycji, a nawet być ponownie używane.
W sekcji poniżej opisujemy poszczególne części i pokazujemy przykład tworzenia niestandardowego modyfikatora do rysowania okręgu.
Modifier.Node
Implementacja Modifier.Node
(w tym przykładzie CircleNode
) wdraża 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 0 lub więcej typów węzłów. Istnieją różne typy węzłów w zależności od funkcji, których wymaga modyfikator. Powyższy przykład musi mieć możliwość rysowania, dlatego implementuje interfejs DrawModifierNode
, który umożliwia zastąpienie metody rysowania.
Dostępne są te typy:
Węzeł |
Wykorzystanie |
Przykładowy link |
Element |
||
|
||
Wdrożenie tego interfejsu umożliwia aplikacji |
||
|
||
|
||
|
||
|
||
|
||
|
||
Może to być przydatne, gdy chcesz połączyć kilka 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 element jest aktualizowany, węzeł wywołuje ponowne rysowanie, a jego kolor jest prawidłowo aktualizowany. Możesz zrezygnować z automatycznego unieważniania, postępując zgodnie z instrukcjami poniżej.
ModifierNodeElement
ModifierNodeElement
to niezmienna klasa, która zawiera dane do utworzenia lub zaktualizowania niestandardowego modyfikatora:
// 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
: ta funkcja tworzy instancję węzła modyfikatora. Ta funkcja jest wywoływana w celu utworzenia węzła, gdy modyfikator jest stosowany po raz pierwszy. Zazwyczaj 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 zmieniła się właściwość. Zależy to od metodyequals
klasy. Wcześniej utworzony węzeł modyfikatora jest wysyłany jako parametr do wywołaniaupdate
. Na tym etapie 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, jakie zapewniaModifier.Node
. Dlatego w metodzieupdate
musisz zaktualizować istniejący węzeł, a nie tworzyć nowego. W naszym przykładzie z okręgiem kolor węzła zostanie zaktualizowany.
Dodatkowo implementacje ModifierNodeElement
muszą też implementować equals
i hashCode
. update
zostanie wywołana tylko wtedy, gdy porównanie z poprzednim elementem za pomocą operatora równości zwróci wartość false.
W przykładzie powyżej 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 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. W większości implementacji wystarczy utworzyć element modyfikatora i dodać go do łańcucha modyfikatorów:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Pełny przykład
Te 3 elementy tworzą niestandardowy modyfikator, który za pomocą interfejsów Modifier.Node
API rysuje okrąg:
// 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ę elementu Modifier.Node
Podczas tworzenia niestandardowych modyfikatorów za pomocą Modifier.Node
możesz napotkać te typowe sytuacje.
Brak parametrów
Jeśli modyfikator nie ma parametrów, nigdy nie wymaga aktualizacji i nie musi być klasą danych. Oto przykładowa implementacja modyfikatora, który stosuje stałą wartość dopełnienia do komponentu:
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 elementów 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 utworzonymi za pomocą fabryki kompozycyjnej jest to, że mogą one odczytywać wartość lokalną 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ła modyfikatora nie obserwują jednak automatycznie zmian stanu. Aby automatycznie reagować na zmianę lokalną kompozycji, możesz odczytać jej bieżącą wartość w zakresie:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
iIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
W tym przykładzie obserwujemy wartość LocalContentColor
, aby narysować tło na podstawie jego koloru. Funkcja ContentDrawScope
obserwuje zmiany w migawce, więc automatycznie odświeża się, 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ć modyfikator, użyj ObserverModifierNode
.
Na przykład Modifier.scrollable
używa tej techniki do obserwowania zmian w LocalDensity
. Poniżej znajdziesz 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) } }
Modyfikator animacji
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 powyższy 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ć uprawnienia do innych węzłów. Można to wykorzystać na wiele sposobów, np. do wyodrębniania wspólnych implementacji w różnych modyfikatorach, ale można też używać tej funkcji do udostępniania wspólnego stanu w różnych modyfikatorach.
Na przykład podstawowa implementacja węzła modyfikatora, który można kliknąć i 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
Modifier.Node
węzły są automatycznie unieważniane, gdy ich odpowiednie wywołania ModifierNodeElement
są aktualizowane. Czasami w bardziej złożonym modyfikatorze możesz zrezygnować z tego działania, aby mieć większą kontrolę nad tym, kiedy modyfikator unieważnia fazy.
Może to być szczególnie przydatne, jeśli modyfikator niestandardowy modyfikuje zarówno układ, jak i rysowanie. Wyłączenie automatycznego unieważniania pozwala unieważniać tylko rysowanie, gdy zmieniają się tylko właściwości związane z rysowaniem, np. color
, a nie układ.
Może to zwiększyć skuteczność modyfikatora.
Poniżej znajdziesz hipotetyczny przykład z modyfikatorem, który ma lambdy color
, size
i onClick
jako właściwości. Ten modyfikator unieważnia tylko to, co jest wymagane, i pomija unieważnianie tego, co nie jest wymagane:
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) } } }