Tworzenie modyfikatorów niestandardowych

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.
  • 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

LayoutModifierNode

Element Modifier.Node, który zmienia sposób pomiaru i układu zawartości.

Próbka

DrawModifierNode

Modifier.Node, która rysuje w przestrzeni układu.

Próbka

CompositionLocalConsumerModifierNode

Wdrożenie tego interfejsu umożliwia aplikacji Modifier.Node odczytywanie lokalizacji kompozycji.

Próbka

SemanticsModifierNode

Modifier.Node, który dodaje semantyczną parę klucz-wartość do wykorzystania w testowaniu, ułatwieniach dostępu i podobnych przypadkach użycia.

Próbka

PointerInputModifierNode

Modifier.Node, który otrzymuje PointerInputChanges.

Próbka

ParentDataModifierNode

Modifier.Node, który dostarcza dane do układu nadrzędnego.

Próbka

LayoutAwareModifierNode

Modifier.Node, które odbiera wywołania zwrotne onMeasuredonPlaced.

Próbka

GlobalPositionAwareModifierNode

Modifier.Node, który otrzymuje wywołanie zwrotne onGloballyPositioned z ostatecznym LayoutCoordinates układu, gdy globalna pozycja treści mogła ulec zmianie.

Próbka

ObserverModifierNode

Modifier.Node, które implementują ObserverNode, mogą udostępniać własną implementację onObservedReadsChanged, która będzie wywoływana w odpowiedzi na zmiany w obiektach migawek odczytywanych w bloku observeReads.

Próbka

DelegatingNode

Modifier.Node, która może delegować pracę do innych instancji Modifier.Node.

Może to być przydatne, gdy chcesz połączyć kilka implementacji węzłów w jedną.

Próbka

TraversableNode

Umożliwia klasom Modifier.Node przechodzenie w górę i w dół drzewa węzłów w przypadku klas tego samego typu lub określonego klucza.

Próbka

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:

  1. 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.
  2. 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 metody equals klasy. Wcześniej utworzony węzeł modyfikatora jest wysyłany jako parametr do wywołania update. 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 zapewnia Modifier.Node. Dlatego w metodzie update 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ć equalshashCode. 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ć equalshashCode, 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:

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, sizeonClick 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)
        }
    }
}