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 rozszerzenia w Modifier, która udostępnia idiomatyczny interfejs API dla modyfikatora i umożliwia łą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 najprostszym sposobem wdrożenia niestandardowego modyfikatora jest wdrożenie fabryki niestandardowych modyfikatorów, która łączy inne zdefiniowane już fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego działania, zaimplementuj element modyfikatora za pomocą Modifier.Node interfejsów API, 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 modyfikatorów istniejących. Na przykład Modifier.clip() jest implementowany za pomocą modyfikatora graphicsLayer. Ta strategia korzysta z istniejących elementów modyfikatora, a Ty udostępniasz 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ąć w 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 funkcji kompozycyjnej do utworzenia modyfikatora umożliwia też korzystanie z interfejsów API Compose wyższego poziomu, takich jak animate*AsState i inne interfejsy API animacji oparte na stanie Compose. 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))
}

To podejście ma pewne ograniczenia, które zostały opisane w kolejnych sekcjach.

Wartości CompositionLocal są rozwiązywane w miejscu wywołania fabryki modyfikatorów.

Podczas tworzenia niestandardowego modyfikatora za pomocą fabryki modyfikatorów kompozycyjnych lokalne obiekty kompozycji 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. Rozważmy na przykład wspomniany wcześniej przykład lokalnego modyfikatora kompozycji, zaimplementowany nieco inaczej przy użyciu 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 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ą funkcji 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 modyfikatora niestandardowego 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łów modyfikatora.
  • Opcjonalna fabryka modyfikatorów, jak opisano wcześniej.

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. W powyższym przykładzie trzeba narysować coś na ekranie, więc implementuje on interfejs DrawModifierNode, który umożliwia zastąpienie metody draw.

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 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 otrzymuje 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, 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 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: jest to funkcja, która 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. W tym momencie należy zaktualizować właściwości węzłów, aby odpowiadały zaktualizowanym parametrom. Możliwość ponownego wykorzystania węzłów w ten sposób jest kluczowa dla zwiększenia wydajności, jaką 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. Funkcja update zostanie wywołana tylko wtedy, gdy porównanie z poprzednim elementem zwróci wartość false.

W powyższym 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 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 tworzony jest element modyfikatora i dodawany 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 API Modifier.Node 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, nie musi być aktualizowany ani nie musi być klasą danych. Poniżej znajdziesz przykładową implementację modyfikatora, który stosuje stałą ilość 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)
        }
    }
}

Lokalne kompozycje referencyjne

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ść 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 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. 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 Compose Animatable. Na przykład ten fragment kodu modyfikuje wyświetlany wcześniej 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

Węzły Modifier.Node są automatycznie unieważniane, gdy aktualizowane są wywołania ModifierNodeElement. W przypadku złożonych modyfikatorów możesz zrezygnować z tego działania, aby uzyskać większą kontrolę nad tym, kiedy modyfikator unieważnia fazy.

Jest to szczególnie przydatne, jeśli niestandardowy modyfikator 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, np. color. Pozwoli to uniknąć unieważnienia układu i może poprawić skuteczność modyfikatora.

Hipotetyczny przykład pokazujący modyfikator z właściwościami w postaci funkcji lambda color, sizeonClick: Ten modyfikator unieważnia tylko to, co jest wymagane, pomijając niepotrzebne unieważnienia:

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