Tworzenie modyfikatorów niestandardowych

Compose udostępnia wiele gotowych modyfikatorów do typowych zachowań, ale możesz też tworzyć własne modyfikatory.

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 modyfikatora używane przez Compose do modyfikowania interfejsu.
  • Element modyfikatora
    • W tym miejscu możesz zaimplementować zachowanie modyfikatora.

Modyfikator niestandardowy można wdrożyć na kilka sposobów, w zależności od potrzebnej funkcji. Często najprostszym sposobem na wdrożenie modyfikatora niestandardowego jest wdrożenie niestandardowej fabryki modyfikatorów, która łączy inne, już zdefiniowane fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego zachowania, 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 istniejących modyfikatorów. Na przykład Modifier.clip() jest implementowany za pomocą modyfikatora graphicsLayer. Ta strategia wykorzystuje istniejące elementy modyfikatora, a Ty 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)

Jeśli często powtarzasz tę samą grupę modyfikatorów, możesz je opakować we własny modyfikator:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Tworzenie modyfikatora niestandardowego za pomocą fabryki modyfikatorów funkcji kompozycyjnych

Możesz też utworzyć modyfikator niestandardowy za pomocą funkcji typu „composable”, aby przekazywać wartości do istniejącego modyfikatora. Jest to tzw. fabryka modyfikatorów funkcji 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 oparte na stanie Compose. Na przykład ten fragment kodu pokazuje modyfikator, który animuje zmianę kanału alfa 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 metodą ułatwiającą podawanie wartości domyślnych z CompositionLocal, najprostszym sposobem jego wdrożenia jest użycie fabryki modyfikatorów funkcji kompozycyjnej:

@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 opisujemy w sekcjach poniżej.

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

Podczas tworzenia modyfikatora niestandardowego za pomocą fabryki modyfikatorów kompozycyjnych zmienne lokalne kompozycji przyjmują wartość z drzewa kompozycji, w którym są tworzone, a nie używane. Może to prowadzić do nieoczekiwanych wyników. Rozważmy na przykład wspomniany wcześniej przykład modyfikatora lokalnego kompozycji, który został zaimplementowany nieco inaczej przy użyciu funkcji typu „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ż elementy lokalne kompozycji zostaną prawidłowo rozwiązane w miejscu użycia i można je bezpiecznie podnieść.

Modyfikatory funkcji typu „composable” nigdy nie są pomijane

Modyfikatory fabryki funkcji typu „composable” nigdy nie są pomijane, ponieważ nie można pomijać funkcji typu „composable”, które zwracają wartości. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdej rekompozycji, co może być kosztowne, jeśli rekompozycja występuje często.

Modyfikatory funkcji typu „composable” muszą być wywoływane w funkcji typu „composable”

Podobnie jak wszystkie funkcje typu „composable”, funkcja fabryczna modyfikatora typu „composable” musi być wywoływana z poziomu kompozycji. Ogranicza to miejsce, do którego można przenieść modyfikator, ponieważ nie można go przenieść poza kompozycję. Dla porównania, fabryki modyfikatorów, które nie są typu „composable”, można wyciągnąć z funkcji typu „composable”, 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
}

Zaimplementuj niestandardowe zachowanie 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, i najbardziej wydajny sposób tworzenia modyfikatorów niestandardowych.

Wdrażanie modyfikatora niestandardowego za pomocą Modifier.Node

Wdrożenie 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 są bezstanowe, a nowe instancje są przydzielane przy każdej rekompozycji, natomiast klasy Modifier.Node mogą być stanowe i przetrwają wiele rekompozycji, a nawet mogą być używane wielokrotnie.

W następnej sekcji opisujemy każdą część i pokazujemy przykład tworzenia niestandardowego modyfikatora do rysowania okręgu.

Modifier.Node

Implementacja Modifier.Node (w tym przykładzie CircleNode) implementuje 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 co najmniej 1 typ węzła. Dostępne są różne typy węzłów w zależności od funkcji, której wymaga modyfikator. Poprzedni przykład musi mieć możliwość rysowania, więc implementuje interfejs DrawModifierNode, który umożliwia zastąpienie metody rysowania.

Dostępne są te typy:

Węzeł

Wykorzystanie

Przykładowy link

LayoutModifierNode

Modifier.Node, który zmienia sposób pomiaru i układu zawartej w nim treści.

Próbka

DrawModifierNode

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

Próbka

CompositionLocalConsumerModifierNode

Zaimplementowanie tego interfejsu umożliwia Modifier.Node odczytywanie zmiennych lokalnych kompozycji.

Próbka

SemanticsModifierNode

Modifier.Node, który dodaje parę klucz-wartość semantyki do użycia w testach, 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óry otrzymuje wywołania zwrotne onMeasured i onPlaced.

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 obiektów zrzutu odczytywanych w bloku observeReads.

Próbka

DelegatingNode

Modifier.Node, który może przekazywać zadania innym instancjom Modifier.Node.

Może to być przydatne do połączenia wielu 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 odpowiedniego elementu. Nasz przykład to DrawModifierNode, więc za każdym razem, gdy wywoływana jest aktualizacja elementu, węzeł wyzwala ponowne rysowanie i 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 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 muszą zastąpić te metody:

  1. create: 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. Zwykle 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 właściwość uległa zmianie. Określa to metoda equals klasy. Węzeł modyfikatora, który został wcześniej utworzony, 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 użycia węzłów w ten sposób jest kluczowa dla zwiększenia wydajności, które zapewnia Modifier.Node. Dlatego musisz zaktualizować istniejący węzeł, zamiast tworzyć nowy w metodzie update. W naszym przykładzie z okręgiem kolor węzła zostanie zaktualizowany.

Dodatkowo implementacje ModifierNodeElement muszą też implementować equals i hashCode. Metoda update zostanie wywołana tylko wtedy, gdy porównanie z poprzednim elementem zwróci wartość false.

W poprzednim przykładzie użyto w tym celu 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ć metody equals i hashCode, na przykład element modyfikatora dopełnienia.

Fabryka modyfikatorów

To jest publiczny interfejs API modyfikatora. Większość implementacji tworzy element modyfikatora i dodaje go do łańcucha modyfikatorów:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

Kompletny przykład

Te 3 części tworzą niestandardowy modyfikator 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)
    }
}

Typowe sytuacje, w których używa się Modifier.Node

Podczas tworzenia modyfikatorów niestandardowych za pomocą Modifier.Node możesz napotkać kilka typowych sytuacji.

Zero parametrów

Jeśli modyfikator nie ma parametrów, nigdy nie musi być aktualizowany i nie musi być klasą danych. Poniżej znajdziesz przykładową implementację modyfikatora, który stosuje stałą wartość dopełnienia do elementu kompozycyjnego:

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 ustawienia kompozycji referencyjnej

Modyfikatory Modifier.Node nie obserwują automatycznie zmian w obiektach stanu Compose, takich jak CompositionLocal. Modyfikatory Modifier.Node mają przewagę nad modyfikatorami utworzonymi za pomocą funkcji kompozycyjnej, ponieważ mogą odczytywać wartość kompozycji lokalnej z miejsca, w którym modyfikator jest używany w drzewie interfejsu, a nie z miejsca, w którym jest przydzielony, za pomocą currentValueOf.

Jednak instancje węzłów modyfikatora nie obserwują automatycznie zmian stanu. Aby automatycznie reagować na zmianę zmiennej lokalnej kompozycji, możesz odczytać jej bieżącą wartość w zakresie:

W tym przykładzie wartość LocalContentColor jest obserwowana, aby narysować tło na podstawie koloru. Ponieważ ContentDrawScope obserwuje zmiany w zdjęciu, automatycznie przerysowuje się, gdy zmienia 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:

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 pokazany 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ć zadania do innych węzłów. Istnieje wiele przypadków użycia, takich jak wyodrębnianie wspólnych implementacji w różnych modyfikatorach, ale można go też używać do udostępniania wspólnego stanu w modyfikatorach.

Oto przykład podstawowej implementacji węzła modyfikatora klikalnego, 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ą odpowiadające im wywołania ModifierNodeElement. W przypadku złożonych modyfikatorów możesz zrezygnować z tego zachowania, aby uzyskać bardziej szczegółową kontrolę nad tym, kiedy modyfikator unieważnia fazy.

Jest to szczególnie przydatne, jeśli modyfikator niestandardowy modyfikuje zarówno układ, jak i rysowanie. Rezygnacja z automatycznego unieważniania pozwala unieważnić rysowanie tylko wtedy, gdy zmieniają się właściwości związane z rysowaniem, takie jak color. Dzięki temu unikniesz unieważnienia układu i możesz zwiększyć wydajność modyfikatora.

Hipotetyczny przykład pokazano poniżej z modyfikatorem, który ma właściwości lambda color, size i onClick. 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)
        }
    }
}