Funkcja tworzenia zawiera od razu wiele modyfikatorów typowych dla typowych działań, ale możesz też tworzyć własne modyfikatory niestandardowe.
Modyfikatory składają się z wielu części:
- Fabryka modyfikatorów.
- To jest funkcja rozszerzenia w funkcji
Modifier
, która zapewnia idiomatyczny interfejs API dla modyfikatora i umożliwia łatwe łączenie modyfikatorów. Fabryka modyfikatorów tworzy elementy modyfikujące służące do modyfikowania interfejsu użytkownika.
- To jest funkcja rozszerzenia w funkcji
- Element modyfikujący:
- W tym miejscu możesz zastosować działanie modyfikatora.
Modyfikator niestandardowy można wdrożyć na wiele sposobów w zależności od potrzebnej funkcjonalności. Często najprostszym sposobem wdrożenia modyfikatora niestandardowego jest po prostu wdrożenie fabryki modyfikatorów niestandardowych, która łączy ze sobą inne już zdefiniowane fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego działania, zaimplementuj element modyfikujący za pomocą interfejsów API Modifier.Node
, które są niższego poziomu, ale zapewniają większą elastyczność.
Połącz istniejące modyfikatory ze sobą
Często można utworzyć niestandardowe modyfikatory za pomocą istniejących już modyfikacji. Na przykład obiekt Modifier.clip()
jest implementowany z wykorzystaniem modyfikatora graphicsLayer
. Ta strategia wykorzystuje dotychczasowe elementy modyfikatorów
i 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)
A jeśli często powtarzasz tę samą grupę modyfikatorów, możesz dodać je do własnego modyfikatora:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Tworzenie modyfikatora niestandardowego za pomocą fabryki modyfikatorów kompozycyjnych
Możesz też utworzyć modyfikator niestandardowy za pomocą funkcji kompozycyjnej, aby przekazywać wartości do istniejącego modyfikatora. Jest to tzw. fabryka modyfikatorów kompozycyjnych.
Utworzenie modyfikatora za pomocą fabryki modyfikatorów kompozycyjnych umożliwia też korzystanie z interfejsów API wyższego poziomu do tworzenia, takich jak animate*AsState
i inne interfejsy API animacji wspieranych przez stan tworzenia. Na przykład ten fragment kodu pokazuje modyfikator, który animuje zmianę w wersji alfa po włączeniu/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 to wygodna metoda dostarczania wartości domyślnych z CompositionLocal
, najprostszym sposobem na wdrożenie modyfikatora jest użycie fabryki modyfikatorów kompozycyjnych:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
To podejście wiąże się z pewnymi zastrzeżeniami opisanymi poniżej.
Wartości CompositionLocal
są rozstrzygane w witrynie wywołania fabryki modyfikatorów
Gdy tworzysz modyfikator niestandardowy przy użyciu fabryki modyfikatorów kompozycyjnych, lokalni kompozytorzy wybierają wartość z drzewa kompozycji, w którym zostały utworzone, a nie używane. Może to prowadzić do nieoczekiwanych wyników. Weźmy np. powyższy przykład lokalnego modyfikatora kompozycji, implementowany nieco inaczej za pomocą funkcji kompozycyjnej:
@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 według Ciebie modyfikator będzie działać w inny sposób, użyj niestandardowego Modifier.Node
, ponieważ lokalne kompozycje zostaną poprawnie rozpoznane na stronie użycia i można je bezpiecznie przenieść.
Modyfikatory funkcji kompozycyjnych nigdy nie są pomijane
Modyfikatory kompozycyjne modyfikatory fabryczne nigdy nie są pomijane, ponieważ nie można pominąć funkcji kompozycyjnych, które mają wartości zwracane. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdej zmianie kompozycji, co może być kosztowne, jeśli będzie się często rekomponować.
Modyfikatory funkcji kompozycyjnych muszą być wywoływane w ramach funkcji kompozycyjnej
Podobnie jak wszystkie funkcje kompozycyjne, modyfikator fabryczny funkcji kompozycyjnej musi być wywoływany z poziomu kompozycji. Ogranicza to miejsce, do którego można przenieść modyfikator, ponieważ nie można go usunąć z kompozycji. Dla porównania fabryki modyfikatorów niekompozycyjnych można przenosić z funkcji kompozycyjnych, aby ułatwić ponowne ich wykorzystanie 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 }
Stosowanie działania modyfikatora niestandardowego za pomocą elementu Modifier.Node
Modifier.Node
to interfejs API niższego poziomu do tworzenia modyfikatorów w funkcji tworzenia wiadomości. To ten sam interfejs API, w którym funkcja Compose implementuje własne modyfikatory, i jest najskuteczniejszym sposobem tworzenia modyfikatorów niestandardowych.
Stosowanie modyfikatora niestandardowego za pomocą elementu Modifier.Node
Implementacja modyfikatora niestandardowego za pomocą elementu Modifier.Node składa się z 3 etapów:
- Implementacja
Modifier.Node
, która zawiera logikę i stan modyfikatora. ModifierNodeElement
, który tworzy i aktualizuje instancje węzłów z modyfikatorem.- opcjonalną fabrykę modyfikatorów (szczegóły powyżej).
Klasy ModifierNodeElement
są bezstanowe, a do każdej zmiany są przydzielane nowe instancje, natomiast klasy Modifier.Node
mogą być stanowe i mogą funkcjonować niezależnie od wielu zmian kompozycji, a nawet ponownie używać.
W tej sekcji opisujemy każdą część i pokazujemy, jak utworzyć modyfikator niestandardowy 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 rysujemy okrąg z kolorem przekazanym do funkcji modyfikatora.
Węzeł implementuje typ węzła 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 dany modyfikator. Powyższy przykład musi mieć możliwość rysowania, dlatego implementuje właściwość DrawModifierNode
, która umożliwia zastąpienie metody rysowania.
Dostępne typy:
Węzeł |
Wykorzystanie |
Przykładowy link |
|
||
|
||
Po wdrożeniu tego interfejsu |
||
Element |
||
|
||
Element |
||
Numer |
||
Element |
||
Obiekty |
||
Użytkownik 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 po wywołaniu aktualizacji odpowiadającego im elementu. W naszym przykładzie to DrawModifierNode
, więc przy każdej aktualizacji elementu wywoływane jest ponowne rysowanie węzła, a jego kolor jest prawidłowo aktualizowany. Możesz zrezygnować z automatycznego unieważniania, jak opisano poniżej.
ModifierNodeElement
ModifierNodeElement
to stała klasa, która zawiera dane służące do tworzenia lub aktualizowania 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
wymagają zastąpienia tych metod:
create
: funkcja, która tworzy instancję węzła modyfikatora. Jest ono wywoływane do utworzenia węzła po pierwszym zastosowaniu modyfikatora. Zwykle polega to na konstruowaniu węzła i konfigurowaniu go za pomocą parametrów przekazanych do fabryki modyfikatorów.update
: ta funkcja jest wywoływana za każdym razem, gdy ten modyfikator jest podany w tym samym miejscu, w którym ten węzeł już istnieje, ale właściwość uległa zmianie. Zależą one od metodyequals
klasy. Utworzony wcześniej węzeł modyfikatora jest wysyłany jako parametr do wywołaniaupdate
. Na tym etapie musisz zaktualizować właściwości węzłów, tak aby odpowiadały zaktualizowanym parametrom. Możliwość ponownego wykorzystywania węzłów w ten sposób jest kluczem do zwiększenia wydajności, jakie przynosiModifier.Node
, dlatego musisz zaktualizować istniejący węzeł, a nie tworzyć nowy za pomocą metodyupdate
. W przykładzie okręgu aktualizowany jest kolor węzła.
Dodatkowo implementacje ModifierNodeElement
muszą też implementować equals
i hashCode
. Funkcja update
zostanie wywołana tylko wtedy, gdy porównanie równości z poprzednim elementem zwróci wartość fałsz.
W tym celu użyto klasy danych w przykładzie powyżej. 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 konieczność aktualizacji węzła, lub jeśli ze względu na zgodność plików binarnych chcesz unikać klas danych, możesz ręcznie zaimplementować equals
i hashCode
, np. element modyfikatora dopełnienia.
Fabryka modyfikatorów
To jest publiczna powierzchnia modyfikatora interfejsu API. W większości implementacji tworzy się po prostu element modyfikatora i dodaje go do łańcucha modyfikatorów:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Pełny przykład
Te 3 elementy łączą się, aby utworzyć 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) } }
Częste sytuacje związane z użyciem funkcji Modifier.Node
Podczas tworzenia modyfikatorów niestandardowych za pomocą parametru Modifier.Node
możesz natrafić na kilka typowych sytuacji.
Zero parametrów
Jeśli modyfikator nie ma parametrów, nie musi być aktualizowany. Nie musi też być klasą danych. Oto przykładowa implementacja modyfikatora, który stosuje stałą ilość dopełnienia do funkcji kompozycyjnej:
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łania do lokalnych kompozycji
Modyfikatory Modifier.Node
nie rejestrują automatycznie zmian w obiektach stanu tworzenia, np. CompositionLocal
. Modyfikatory Modifier.Node
mają przewagę nad modyfikatorami nowo utworzonymi w fabryce kompozycyjnej. Umożliwiają one odczytywanie wartości kompozycji lokalnej z miejsca, w którym modyfikator jest używany w drzewie interfejsu, a nie w miejscu, w którym jest przydzielony modyfikator (za pomocą funkcji currentValueOf
).
Jednak instancje węzłów modyfikujących nie rejestrują automatycznie zmian stanu. Aby automatycznie zareagować na lokalną zmianę kompozycji, możesz odczytać jej bieżącą wartość w zakresie:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
iIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
W tym przykładzie obserwujemy wartość LocalContentColor
, która pozwala rysować tło na podstawie jego koloru. Ponieważ ContentDrawScope
obserwuje zmiany w zrzucie dysku, automatycznie jest odświeżane po zmianie wartości LocalContentColor
:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Aby reagować na zmiany stanu spoza zakresu i automatycznie aktualizować modyfikator, użyj ObserverModifierNode
.
Na przykład narzędzie Modifier.scrollable
używa tej metody do obserwowania zmian w elemencie LocalDensity
. Poniżej przedstawiono 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 animowania
Modifier.Node
implementacji ma dostęp do interfejsu coroutineScope
. Umożliwia to korzystanie z interfejsów API do tworzenia animacji. Ten fragment modyfikuje na przykład z góry element CircleNode
, aby wielokrotnie się pojawiać i wyciszać:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private val alpha = Animatable(1f) override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Udostępnianie stanu między modyfikatorami przy użyciu przekazywania dostępu
Modyfikatory typu Modifier.Node
mogą przekazywać dostęp do innych węzłów. Ta metoda ma wiele zastosowań, np. wyodrębnianie typowych implementacji z różnymi modyfikatorami, ale można jej też używać do dzielenia stanu w przypadku różnych modyfikatorów.
Na przykład podstawowa implementacja klikalnego węzła modyfikatora, 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
Modifier.Node
węzły są unieważniane automatycznie po aktualizacji odpowiednich wywołań funkcji ModifierNodeElement
. Czasami w bardziej złożonym modyfikatorze można zrezygnować z tego działania, aby uzyskać bardziej precyzyjną kontrolę nad tym, kiedy modyfikator unieważnia fazy.
Jest to szczególnie przydatne, jeśli modyfikator niestandardowy zmienia zarówno układ, jak i rysowanie. Rezygnacja z automatycznego unieważniania umożliwia unieważnienie rysowania tylko wtedy, gdy
właściwości związane z rysowaniem, takie jak color
, zmienianie, a nie unieważnianie układu.
Może to polepszyć skuteczność modyfikatora.
Hipotetyczny przykład takiego żądania jest podany poniżej z modyfikatorem, który ma właściwości color
, size
i onClick
. Ten modyfikator unieważnia tylko wymagane dane i pomija te, które nie:
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) } } }