Tworzenie modyfikatorów niestandardowych

Tworzenie wiadomości od razu udostępnia wiele modyfikatorów typowych działań, ale możesz też tworzyć własne modyfikatory niestandardowe.

Modyfikatory składają się z kilku części:

  • Fabryka modyfikatorów
    • To jest funkcja rozszerzenia Modifier, która udostępnia idiomatyczny interfejs API . Fabryka modyfikatorów tworzy elementy modyfikujące używane przez funkcję Compose do modyfikacji w interfejsie użytkownika.
  • Element modyfikujący
    • Tutaj możesz zastosować działanie modyfikatora.

Istnieje wiele sposobów na zastosowanie modyfikatora niestandardowego w zależności od niezbędną funkcjonalność. Często najłatwiejszym sposobem zastosowania modyfikatora niestandardowego jest aby wdrożyć fabrykę modyfikatorów niestandardowych, która łączy inne fabryki modyfikatorów razem. Jeśli potrzebujesz bardziej niestandardowych działań, zaimplementuj funkcję element modyfikujący za pomocą interfejsów API Modifier.Node, które są niższego poziomu, ale które zapewniają większą elastyczność.

Połącz istniejące modyfikatory

Często można utworzyć niestandardowe modyfikatory, wykorzystując istniejące modyfikatory. Na przykład zaimplementowano Modifier.clip() za pomocą parametru Modyfikator graphicsLayer. Ta strategia korzysta z istniejących elementów modyfikujących, a Ty udostępnić własną fabrykę modyfikatorów niestandardowych.

Zanim zastosujesz własny modyfikator niestandardowy, sprawdź, czy możesz go użyć strategii ustalania stawek.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

A jeśli stwierdzisz, że powtarzasz często tę samą grupę modyfikatorów, umieść je we własnym modyfikatorze:

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

Tworzenie modyfikatora niestandardowego przy użyciu fabryki modyfikatorów kompozycyjnych

Możesz też utworzyć modyfikator niestandardowy, używając funkcji kompozycyjnej do przekazywania wartości. do istniejącego modyfikatora. Nazywamy to fabryką modyfikatorów kompozycyjnych.

Zastosowanie fabryki modyfikatora kompozycyjnego do utworzenia modyfikatora umożliwia też korzystanie z interfejsów API do tworzenia wyższego poziomu, takich jak animate*AsState i inne Compose interfejsów API animacji opartych na stanie. Na przykład ten fragment kodu zawiera modyfikator animujący zmianę 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 niestandardowy modyfikator to dogodna metoda podawania wartości domyślnych z CompositionLocal. Najłatwiejszym sposobem wdrożenia tego rozwiązania jest użycie funkcji kompozycyjnej Fabryka modyfikatorów:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Takie podejście wiąże się z pewnymi zastrzeżeniami, które opisujemy poniżej.

CompositionLocal wartości zostało rozwiązane w witrynie połączenia z fabryką modyfikatorów

Podczas tworzenia niestandardowego modyfikatora przy użyciu fabryki modyfikatorów kompozycyjnych, kompozycja lokalne pobierają wartość z drzewa kompozycji, w którym zostały utworzone, a nie . Może to prowadzić do nieoczekiwanych rezultatów. Weźmy na przykład kompozycję przykład lokalnego modyfikatora, zaimplementowany nieco inaczej za pomocą funkcja kompozycyjna:

@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 tak działa modyfikator, użyj Modifier.Node, ponieważ lokalne zasoby kompozycji zostały rozwiązane prawidłowo w miejscu użytkowania i można je bezpiecznie podnosić.

Kompozycyjne modyfikatory funkcji nie są nigdy pomijane

Kompozycyjne modyfikatory fabryczne nigdy nie są pomijane, ponieważ funkcje kompozycyjne , które zwracają wartości, nie mogą zostać pominięte. Oznacza to, że funkcja modyfikatora będzie wywoływane przy każdej zmianie, co może być drogie, jeśli przekomponuje się często.

Modyfikatory funkcji kompozycyjne muszą być wywoływane w obrębie funkcji kompozycyjnej

Tak jak w przypadku wszystkich funkcji kompozycyjnych, modyfikator fabryczny z możliwością kompozycyjnej musi być wywoływany z w obrębie kompozycji. Ogranicza to miejsce, do którego można podnieść modyfikator, ponieważ nigdy nie zostaną wypchnięte poza kompozycję. Dla porównania: modyfikator niekompozycyjny fabryki można wyciągnąć z funkcji kompozycyjnych, aby ułatwić ponowne Popraw 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. it interfejsu API, w którym Compose implementuje własne modyfikatory. wydajniejszy sposób tworzenia modyfikatorów niestandardowych.

Zastosuj modyfikator niestandardowy, używając funkcji Modifier.Node

Wdrożenie modyfikatora niestandardowego za pomocą modyfikatora.Węzeł składa się z 3 etapów:

  • Implementacja Modifier.Node, która zawiera logikę stanu modyfikatora.
  • ModifierNodeElement, który tworzy i aktualizuje modyfikator instancji węzłów.
  • Opcjonalna fabryka modyfikatorów, jak opisano powyżej.

ModifierNodeElement klasa jest bezstanowa, a każda z nich jest przydzielana nowe instancje przekomponowanie, natomiast klasy Modifier.Node mogą być stanowe i przetrwać na wiele zmian, a nawet można je wykorzystać ponownie.

W poniższej sekcji opisano każdą z nich i przedstawiamy przykład tworzenia niestandardowy modyfikator, aby narysować okrąg.

Modifier.Node

Implementacja Modifier.Node (w tym przykładzie CircleNode) implementuje: funkcji modyfikatora niestandardowego.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

W tym przykładzie rysuje ono okrąg z kolorem przekazanym do modyfikatora. .

Węzeł implementuje interfejs Modifier.Node oraz zero lub więcej typów węzłów. Istnieją różnych typów węzłów w zależności od funkcji wymaganej przez modyfikator. z powyższego przykładu musi mieć możliwość rysowania, więc implementuje funkcję DrawModifierNode, która umożliwia zastąpienie metody rysowania.

Dostępne typy:

Węzeł

Wykorzystanie

Przykładowy link

LayoutModifierNode

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

Przykład

DrawModifierNode

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

Przykład

CompositionLocalConsumerModifierNode

Wdrożenie tego interfejsu umożliwia urządzeniu Modifier.Node odczytywanie lokalnych kompozycji.

Przykład

SemanticsModifierNode

Modifier.Node z parą klucz-wartość semantyczną, która może być używana w testowaniu, ułatwieniach dostępu i w podobnych przypadkach użycia.

Przykład

PointerInputModifierNode

Modifier.Node, który otrzymuje PointerInputChanges.

Przykład

ParentDataModifierNode

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

Przykład

LayoutAwareModifierNode

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

Przykład

GlobalPositionAwareModifierNode

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

Przykład

ObserverModifierNode

Modifier.Node z implementacją ObserverNode mogą udostępnić własną implementację funkcji onObservedReadsChanged, która będzie wywoływana w odpowiedzi na zmiany obiektów zrzutów odczytywanych w bloku observeReads.

Przykład

DelegatingNode

Pole Modifier.Node, które może przekazywać pracę innym instancji Modifier.Node.

Może to być przydatne, gdy chcesz utworzyć wiele implementacji węzłów w jedną.

Przykład

TraversableNode

Umożliwia klasom Modifier.Node poruszanie się po drzewie węzłów w górę i w dół w przypadku klas tego samego typu lub konkretnego klucza.

Przykład

Węzły są automatycznie unieważniane po wywołaniu aktualizacji . Ponieważ nasz przykład to DrawModifierNode, wywoływana jest każda aktualizacja o każdej porze. elementu, węzeł aktywuje ponowne rysowanie, a jego kolor zostanie prawidłowo zaktualizowany. Jest zrezygnować z automatycznej dezaktywacji, postępując zgodnie z instrukcjami opisanymi poniżej.

ModifierNodeElement

ModifierNodeElement to stała klasa, która przechowuje dane do utworzenia lub zaktualizuj modyfikator niestandardowy:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

Implementacje typu ModifierNodeElement muszą zastąpić te metody:

  1. create: to funkcja tworząca instancję węzła modyfikatora. Dzięki temu w celu utworzenia węzła przy pierwszym zastosowaniu modyfikatora. Zwykle sprowadza się do utworzenia węzła i skonfigurowania go za pomocą parametrów, zostały przekazane do fabryki modyfikatorów.
  2. update: ta funkcja jest wywoływana za każdym razem, gdy ten modyfikator znajduje się w funkcji to samo miejsce, które ten węzeł już istnieje, ale właściwość została zmieniona. To jest jest określana przez metodę equals klasy. Węzeł modyfikujący, który został utworzona wcześniej jest wysyłana jako parametr w wywołaniu update. W tym momencie należy zaktualizować węzły właściwości, które odpowiadają zaktualizowanym . Możliwość ponownego wykorzystania węzłów w ten sposób jest kluczowa wzrost skuteczności kampanii Modifier.Node; dlatego musisz zaktualizować istniejącego węzła, zamiast tworzyć nowy w metodzie update. W naszym okrąg, kolor węzła zostanie zaktualizowany.

Dodatkowo implementacje ModifierNodeElement muszą też implementować equals i hashCode. Funkcja update zostanie wywołana tylko wtedy, gdy argument równa się poprzedni element zwraca wartość false (fałsz).

W tym przykładzie użyto klasy danych. Te metody są wykorzystywane do sprawdź, czy węzeł wymaga aktualizacji. Jeśli element ma właściwości, które mają nie wpływają na to, czy węzeł wymaga aktualizacji, czy też nie chcesz otrzymywać danych ze względu na zgodność binarną, możesz ręcznie zaimplementować funkcję equals i hashCode, np. element modyfikatora dopełnienia.

Fabryka modyfikatora

To jest publiczna powierzchnia interfejsu API modyfikatora. Większość implementacji po prostu utwórz element modyfikujący i dodaj go do łańcucha modyfikatorów:

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

Pełny przykład

Te trzy części się łączą, aby utworzyć modyfikator niestandardowy, który będzie rysować okrąg. 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 podczas korzystania z funkcji Modifier.Node

Oto kilka typowych sytuacji, które mogą wystąpić podczas tworzenia modyfikatorów niestandardowych za pomocą funkcji Modifier.Node spotkania.

Zero parametrów

Jeśli modyfikator nie ma parametrów, nigdy nie musi być aktualizowany. Co więcej, 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)
        }
    }
}

Lokalne kompozycje odsyłające

Modyfikatory funkcji Modifier.Node nie dostrzegają automatycznie zmian stanu tworzenia wiadomości obiekty takie jak CompositionLocal. Modyfikatory korzyści Modifier.Node mają ponad dla modyfikatorów w fabryce kompozycyjnej, wartość kompozycji lokalnej, w której jest używany modyfikator w Twoim interfejsie drzewo, w którym nie przydziela się modyfikator, używając funkcji currentValueOf.

Jednak instancje węzłów z modyfikatorem nie obserwują automatycznie zmian stanu. Do automatycznie reagować na lokalną zmianę kompozycji, można odczytać jej aktualną w zakresie:

W tym przykładzie obserwuje się wartość LocalContentColor, która pozwala narysować tło na podstawie na jego kolor. Ponieważ ContentDrawScope obserwuje zmiany w zrzutach, to automatycznie pobiera ponownie 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ć użyj ObserverModifierNode.

Modifier.scrollable używa tej metody na przykład do: obserwuj zmiany zachodzące w komórce LocalDensity. Poniżej znajduje się 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. Dzięki temu interfejsy Compose animatable API. Na przykład ten fragment zmienia CircleNode z góry, aby stopniowo się i wyłączać:

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 korzystającymi z przekazywania dostępu

Modyfikatory Modifier.Node mogą przekazywać uprawnienia do innych węzłów. Wiele przypadków użycia np. wyodrębnienie wspólnych implementacji z różnymi modyfikatorami, ale może też być używany do współdzielenia wspólnego stanu dla modyfikatorów.

Na przykład podstawowa implementacja klikalnego węzła modyfikatora, który ma taki sam dane interakcji:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Rezygnacja z automatycznej unieważniania węzłów

Modifier.Node węzłów jest unieważnianych automatycznie, jeśli odpowiadające im Aktualizacja połączeń ModifierNodeElement. Czasami w bardziej złożonym modyfikatorze chcesz zrezygnować z tego zachowania, by mieć dokładniejszą kontrolę nad tym, modyfikator unieważnia fazy.

Jest to szczególnie przydatne, gdy modyfikator niestandardowy modyfikuje zarówno układ, remis. Rezygnacja z automatycznej unieważniania pozwoli Ci unieważnić rysowanie, gdy: można zmieniać jedynie właściwości związane z rysowaniem, takie jak color, i nie unieważniać układu. Może to poprawić skuteczność modyfikatora.

Hipotetyczny przykład przedstawiono poniżej z modyfikatorem zawierającym color, size i onClick lambda jako właściwości. Ten modyfikator unieważnia tylko wymagane i pomija wszystkie 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)
        }
    }
}