Tworzenie modyfikatorów niestandardowych

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

LayoutModifierNode

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

Próbka

DrawModifierNode

Modifier.Node, który wyświetla się w przestrzeni układu.

Próbka

CompositionLocalConsumerModifierNode

Po wdrożeniu tego interfejsu Modifier.Node może odczytywać lokalne kompozycje.

Próbka

SemanticsModifierNode

Element Modifier.Node, który dodaje semantyczną parę klucz/wartość na potrzeby testowania, ułatwień dostępu i podobnych przypadków użycia.

Próbka

PointerInputModifierNode

Modifier.Node, który otrzymuje PointerInputChanges.

Próbka

ParentDataModifierNode

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

Próbka

LayoutAwareModifierNode

Numer Modifier.Node, który otrzymuje wywołania zwrotne onMeasured i onPlaced.

Próbka

GlobalPositionAwareModifierNode

Element Modifier.Node, który otrzymuje wywołanie zwrotne onGloballyPositioned z końcowym LayoutCoordinates układu, gdy globalne położenie treści mogło się zmienić.

Próbka

ObserverModifierNode

Obiekty Modifier.Node implementujące ObserverNode mogą mieć własną implementację interfejsu onObservedReadsChanged, która będzie wywoływana w odpowiedzi na zmiany w obiektach zrzutu w bloku observeReads.

Próbka

DelegatingNode

Użytkownik Modifier.Node, który może przekazywać zadania 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 przeglądanie w górę i w dół drzewa węzłów w przypadku klas tego samego typu lub konkretnego klucza.

Próbka

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:

  1. 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.
  2. 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 metody equals klasy. Utworzony wcześniej węzeł modyfikatora jest wysyłany jako parametr do wywołania update. 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 przynosi Modifier.Node, dlatego musisz zaktualizować istniejący węzeł, a nie tworzyć nowy za pomocą metody update. 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:

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