Compose udostępnia wiele modyfikatorów dostępnych od razu po zainstalowaniu, ale możesz też tworzyć własne modyfikatory niestandardowe.
Modyfikatory mają kilka części:
- Modyfikator fabryczny
- To funkcja rozszerzenia w
Modifier
, która udostępnia idiomatyczny interfejs API dla modyfikatora i umożliwia łatwe łańcuchowe łączenie modyfikatorów. Fabryka modyfikatorów generuje elementy modyfikatorów używane przez Compose do modyfikowania interfejsu użytkownika.
- To funkcja rozszerzenia w
- Element modyfikujący
- Tutaj możesz zastosować działanie modyfikatora.
W zależności od potrzebnej funkcjonalności modyfikator niestandardowy można zaimplementować na kilka sposobów. Często najłatwiejszym sposobem wdrożenia niestandardowego modyfikatora jest wdrożenie niestandardowej fabryki modyfikatorów, która łączy ze sobą inne zdefiniowane już fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego zachowania, zaimplementuj element modifier za pomocą interfejsów Modifier.Node
API, które są interfejsami niższego poziomu, ale zapewniają większą elastyczność.
Łączenie dotychczasowych modyfikatorów
Często można tworzyć modyfikatory niestandardowe, korzystając tylko z dotychczasowych modyfikatorów. Na przykład Modifier.clip()
jest implementowane za pomocą modyfikatora graphicsLayer
. Ta strategia wykorzystuje istniejące elementy modyfikujące, a Ty udostępniasz własną fabrykę modyfikatorów niestandardowych.
Zanim wdrożysz własny modyfikator niestandardowy, sprawdź, czy nie możesz użyć tej samej strategii.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Jeśli zauważysz, że często powtarzasz tę samą grupę modyfikatorów, możesz je umieścić w ramach własnego modyfikatora:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Tworzenie niestandardowego modyfikatora za pomocą fabryki modyfikatorów złożonych
Modyfikator niestandardowy możesz też utworzyć za pomocą funkcji kompozycyjnej, która przekazuje wartości do istniejącego modyfikatora. Jest to tzw. fabryka modyfikatorów składanych.
Korzystanie z fabryki modyfikatorów składanych do tworzenia modyfikatorów umożliwia też używanie interfejsów API do tworzenia złożonych interfejsów, takich jak animate*AsState
i inne interfejsy API do tworzenia animacji na podstawie stanu. Na przykład ten fragment kodu zawiera modyfikator, który po włączeniu lub wyłączeniu animuje zmianę alfa:
@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ą na podawanie wartości domyślnych z poziomu CompositionLocal
, najprostszym sposobem na wdrożenie tego rozwiązania jest użycie modułowej fabryki modyfikatorów:
@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.
CompositionLocal
wartości zostało rozwiązane w witrynie połączenia telefonicznego fabryki modyfikatorów
Podczas tworzenia modyfikatora niestandardowego za pomocą fabryki modyfikatorów kompozytowych wartości kompozycji lokalnej są pobierane z drzewa kompozycji, w której są tworzone, a nie z kompozycji, w której są używane. Może to prowadzić do nieoczekiwanych wyników. Weźmy na przykład kompozycję z lokalnym modyfikatorem z powyższego przykładu, która została zaimplementowana nieco inaczej za pomocą funkcji kompozytowej:
@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 taki sposób, użyj zamiast tego modyfikatora niestandardowego Modifier.Node
, ponieważ zmienne kompozycyjne zostaną poprawnie rozwiązane w miejscu użycia i można je bezpiecznie podnosić.
Kompozycyjne modyfikatory funkcji nie są nigdy pomijane
Modyfikatory fabryk składanych nigdy nie są pominane, ponieważ składane funkcje, które zwracają wartości, nie mogą być pomijane. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdym przekształceniu, co może być kosztowne, jeśli będzie się często powtarzać.
Zmienne funkcji typu „composable” muszą być wywoływane w ramach funkcji typu „composable”
Podobnie jak wszystkie funkcje kompozytowe, modyfikator fabryki kompozytowej musi być wywoływany z poziomu kompozycji. Ogranicza to miejsce, do którego może zostać przeniesiony modyfikator, ponieważ nigdy nie może zostać przeniesiony poza kompozycję. Z kolei fabryki modyfikatorów, które nie są kompozytowe, można wyodrębnić z funkcji kompozytowych, 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 }
Zastosuj działanie modyfikatora niestandardowego, korzystając z funkcji Modifier.Node
Modifier.Node
to interfejs API niższego poziomu do tworzenia modyfikatorów w Compose. To ten sam interfejs API, w którym Compose stosuje własne modyfikatory. To najefektywniejszy sposób tworzenia modyfikatorów niestandardowych.
Implementowanie niestandardowego modyfikatora za pomocą Modifier.Node
Implementacja niestandardowego modyfikatora za pomocą węzła Modifier.Node składa się z 3 części:
- Implementacja
Modifier.Node
zawierająca logikę i stan modyfikatora. ModifierNodeElement
, który tworzy i aktualizuje instancje węzła modyfikatora.- Opcjonalna fabryka modyfikatorów opisana powyżej.
Klasy ModifierNodeElement
są stanowe i przy każdej rekompozycji przydzielane są nowe instancje, natomiast klasy Modifier.Node
mogą być stanowe i przetrwają przez wiele rekompozycji, a nawet można je ponownie wykorzystać.
W następnej sekcji 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
) realizuje funkcjonalność niestandardowego modyfikatora.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
W tym przykładzie rysuje on okrąg z kolorem przekazanym do funkcji modyfikatora.
Węzeł implementuje interfejs Modifier.Node
oraz zero lub więcej typów węzłów. Istnieją różne typy węzłów, które zależą od funkcji wymaganych przez modyfikator. Przykład powyżej musi mieć możliwość rysowania, więc implementuje interfejs DrawModifierNode
, który pozwala mu zastąpić metodę draw().
Dostępne typy:
Węzeł |
Wykorzystanie |
Link do przykładu |
|
||
|
||
Dzięki wdrożeniu tego interfejsu aplikacja |
||
|
||
|
||
|
||
|
||
|
||
|
||
Może to być przydatne, gdy chcesz połączyć kilka implementacji węzłów w jedną. |
||
Umożliwia klasom |
Po wywołaniu metody update na odpowiednim elemencie węzły są automatycznie unieważniane. Ponieważ nasz przykład to DrawModifierNode
, przy każdym wywołaniu aktualizacji elementu węzeł aktywuje ponowne renderowanie, a jego kolor zostanie prawidłowo zaktualizowany. Możesz zrezygnować z automatycznej unieważniania, jak opisano poniżej.
ModifierNodeElement
ModifierNodeElement
to niezmienna klasa, która przechowuje dane potrzebne 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ąpić te metody:
create
: to funkcja, która tworzy instancję węzła modyfikatora. Jest on wywoływany, aby utworzyć węzeł, gdy modyfikator zostanie zastosowany po raz pierwszy. Zwykle wymaga to utworzenia węzła i skonfigurowania go za pomocą parametrów, które są przekazywane do fabryki modyfikatorów.update
: ta funkcja jest wywoływana, gdy ten modyfikator jest podany w tym samym miejscu, w którym węzeł już istnieje, ale zmieniła się jakaś właściwość. Jest ona określana przez metodęequals
klasy. Utworzony wcześniej węzeł modyfikatora jest wysyłany jako parametr do wywołaniaupdate
. W tym momencie powinieneś zaktualizować właściwości węzłów, aby odpowiadały one zaktualizowanym parametrom. Możliwość ponownego użycia węzłów w ten sposób jest kluczowa dla wzrostu wydajności, jaki zapewniaModifier.Node
. Dlatego musisz zaktualizować istniejący węzeł, a nie tworzyć nowego za pomocą metodyupdate
. W naszym przykładzie kółko zmienia kolor.
Dodatkowo implementacje ModifierNodeElement
muszą też implementować equals
i hashCode
. Funkcja update
zostanie wywołana tylko wtedy, gdy porównanie z poprzednim elementem zwróci wartość fałsz.
W przykładzie powyżej do tego celu 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ł musi zostać zaktualizowany, lub jeśli chcesz uniknąć klas danych ze względu na zgodność binarną, możesz ręcznie zaimplementować equals
i hashCode
, np. element modyfikatora wypełnienia.
Fabryka modyfikatora
To jest publiczna strona interfejsu API modyfikatora. Większość implementacji po prostu tworzy element modyfikujący 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 tworzą niestandardowy modyfikator do rysowania koła 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 z użyciem Modifier.Node
Oto kilka typowych sytuacji, które mogą wystąpić podczas tworzenia modyfikatorów niestandardowych za pomocą funkcji Modifier.Node
.
Zero parametrów
Jeśli modyfikator nie ma parametrów, nigdy nie trzeba go aktualizować, a dodatkowo nie musi być klasą danych. Oto przykładowa implementacja modyfikatora, który stosuje stałe dopełnienie 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ływanie się do lokalizacji w kompozycji
Modyfikatory Modifier.Node
nie rejestrują automatycznie zmian stanu obiektów w komponencie Compose, takich jak CompositionLocal
. Zaletą modyfikatorów Modifier.Node
w porównaniu z modyfikatorami utworzonymi za pomocą fabryki kompozytowej jest to, że mogą one odczytywać wartość kompozycji lokalnej z miejsca, w którym modyfikator jest używany w drzewie interfejsu użytkownika, a nie z miejsca, w którym jest przypisany, za pomocą currentValueOf
.
Jednak instancje węzła modyfikatora nie rejestrują automatycznie zmian stanu. Aby automatycznie reagować na zmiany w kompozycji lokalnej, 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. Ponieważ ContentDrawScope
obserwuje zmiany w zrzutach, automatycznie jest ono ponownie pobierane, 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 funkcja Modifier.scrollable
używa tej techniki do obserwowania zmian w funkcji LocalDensity
. Poniżej przedstawiamy 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) } }
Animowany modyfikator
Implementacje typu Modifier.Node
mają dostęp do komponentu coroutineScope
. Pozwala to na korzystanie z interfejsów API Compose Animatable. Ten fragment kodu modyfikuje CircleNode
z powyższego przykładu, aby efekt płynnego pojawiania się i znikania był powtarzany:
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 za pomocą delegowania
Modifier.Node
modyfikatory mogą delegować do innych węzłów. Jest wiele przypadków użycia tego rozwiązania, np. wyodrębnianie wspólnych implementacji z różnymi modyfikatorami. Można go też wykorzystać do wyświetlania wspólnego stanu między modyfikatorami.
Oto przykład podstawowej implementacji 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ła
Modifier.Node
są automatycznie nieważne, gdy ich odpowiadające im wywołania ModifierNodeElement
zostaną zaktualizowane. Czasami w przypadku bardziej złożonego modyfikatora możesz chcieć zrezygnować z tego zachowania, aby uzyskać 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żnić tylko draw, gdy zmieniają się tylko właściwości związane z draw, np. color
, a nie unieważniać układu.
Może to poprawić skuteczność modyfikatora.
Poniżej przedstawiamy hipotetyczny przykład modyfikatora z właściwościami color
, size
i lambda onClick
. Unieważnia on tylko to, co jest wymagane, i pomija te unieważnienia, które nie są:
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) } } }