Tworzenie modyfikatorów niestandardowych

Funkcja tworzenia 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 modyfikatora i umożliwia łatwe łączenie modyfikatorów ze sobą. Fabryka modyfikatorów tworzy elementy modyfikujące używane przez funkcję Compose do modyfikowania interfejsu użytkownika.
  • Element modyfikujący
    • Tutaj możesz zastosować działanie modyfikatora.

Istnieje wiele sposobów implementacji modyfikatora niestandardowego w zależności od potrzebnej funkcjonalności. Często najłatwiejszym sposobem wdrożenia modyfikatora niestandardowego jest zastosowanie fabryki modyfikatorów niestandardowych, która łączy 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 dają większą elastyczność.

Połącz istniejące modyfikatory

Często można utworzyć niestandardowe modyfikatory, korzystając z istniejących modyfikatorów. Na przykład reguła Modifier.clip() została zaimplementowana z użyciem modyfikatora graphicsLayer. Ta strategia wykorzystuje istniejące elementy modyfikujące, 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 dodać je do własnego modyfikatora:

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

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

Modyfikator niestandardowy możesz też utworzyć za pomocą funkcji kompozycyjnej, która przekazuje wartości do istniejącego modyfikatora. Nazywamy to fabryką modyfikatorów kompozycyjnych.

Użycie fabryki modyfikatora kompozycyjnego do utworzenia modyfikatora umożliwia też korzystanie z interfejsów API tworzenia wyższego poziomu, takich jak animate*AsState i inne interfejsy API animacji oparte na stanie tworzenia. Na przykład ten fragment kodu zawiera modyfikator, który po włączeniu lub wyłączeniu animuje zmianę alfa:

@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 podawania wartości domyślnych z CompositionLocal, najprostszym sposobem na wdrożenie modyfikatora jest użycie fabryki modyfikatora kompozycyjnego:

@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 modyfikatora niestandardowego z użyciem fabryki modyfikatora kompozycyjnego lokalne obiekty kompozycji przyjmują wartość z drzewa kompozycji, w którym zostały utworzone, a nie są używane. Może to prowadzić do nieoczekiwanych rezultatów. Weźmy na przykład opisany powyżej przykład lokalny modyfikator kompozycji zaimplementowany 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 nie tak działa Twój modyfikator, użyj niestandardowego parametru Modifier.Node, ponieważ lokalne kompozycje będą prawidłowo rozpoznawane w miejscu użycia i będzie można je bezpiecznie podnieść.

Kompozycyjne modyfikatory funkcji nie są nigdy pomijane

Kompozycyjne modyfikatory fabryczne nigdy nie są pomijane, ponieważ funkcje kompozycyjne, które mają wartości zwracane, nie mogą być pomijane. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdej zmianie kompozycji. Może to być kosztowne w przypadku częstego ponownego tworzenia kompozycji.

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

Tak jak w przypadku wszystkich funkcji kompozycyjnych modyfikator fabryczny musi być wywoływany z poziomu kompozycji. Ogranicza to miejsce, do którego można podnieść modyfikator, ponieważ nigdy nie można go wyciągnąć z kompozycji. Dla porównania fabryki modyfikatorów niekompozycyjnych można wyciągnąć z funkcji kompozycyjnych, aby ułatwić ponowne wykorzystanie 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
}

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. To ten sam interfejs API, w którym usługa Compose implementuje własne modyfikatory. To najefektywniejszy 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 zawierająca 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 są bezstanowe, a nowe instancje są przydzielane do każdej ponownej kompozycji, natomiast klasy Modifier.Node mogą być stanowe i przetrwać w różnych wersjach, a nawet można je wykorzystać ponownie.

W tej sekcji opisujemy każdą część i pokazujemy przykład tworzenia niestandardowego modyfikatora, który pozwoli narysować okrąg.

Modifier.Node

Implementacja Modifier.Node (w tym przykładzie CircleNode) implementuje działanie modyfikatora niestandardowego.

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

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

Węzeł implementuje interfejs Modifier.Node oraz zero lub więcej typów węzłów. Istnieją różne typy węzłów w zależności od funkcji wymaganej przez modyfikator. W przykładzie powyżej nie można rysować, więc implementuje funkcję DrawModifierNode, która zastępuje metodę 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 odpowiadającego im elementu. Ponieważ nasz przykład to DrawModifierNode, przy każdym wywołaniu aktualizacji elementu węzeł aktywuje ponowne renderowanie, a jego kolor zostanie prawidłowo zaktualizowany. Możesz zrezygnować z automatycznej unieważniania, jak opisano poniżej.

ModifierNodeElement

ModifierNodeElement to stała klasa, która przechowuje dane służące 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 typu ModifierNodeElement muszą zastąpić te metody:

  1. create: to funkcja tworząca instancję węzła modyfikatora. Jest ona wywoływana w celu utworzenia węzła przy pierwszym zastosowaniu modyfikatora. Zwykle wymaga to utworzenia węzła i skonfigurowania go za pomocą parametrów, które są przekazywane do fabryki modyfikatorów.
  2. update: ta funkcja jest wywoływana za każdym razem, gdy ten modyfikator występuje w tym samym miejscu, w którym ten węzeł już istnieje, ale właściwość uległa zmianie. Jest to określone przez metodę equals klasy. Utworzony wcześniej węzeł modyfikujący jest wysyłany jako parametr do wywołania update. Na tym etapie musisz 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 kluczem do poprawy wydajności, jaką zapewnia Modifier.Node. Dlatego musisz zaktualizować istniejący węzeł, zamiast tworzyć nowy za pomocą metody update. W naszym przykładzie koła 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 z poprzednim elementem zwróci wartość fałsz.

W tym przykładzie użyto klasy danych. Te metody służą do sprawdzania, czy węzeł wymaga aktualizacji. Jeśli Twój 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ść plików binarnych, możesz ręcznie zaimplementować equals i hashCode, np. element modyfikatora dopełnienia.

Fabryka modyfikatora

To jest publiczna powierzchnia interfejsu API modyfikatora. Większość implementacji po prostu tworzy element modyfikujący 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 części łączą się w niestandardowy modyfikator służący 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 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.

Zero parametrów

Jeśli modyfikator nie ma parametrów, nigdy nie wymaga aktualizacji i nie musi też 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 Modifier.Node nie dostrzegają automatycznie zmian w obiektach stanu tworzenia, takich jak CompositionLocal. Zaletą modyfikatorów Modifier.Node, które właśnie tworzy się przy użyciu fabryki kompozycyjnej, jest to, że mogą odczytywać wartość kompozycji lokalnie z miejsca, w którym jest on używany w drzewie interfejsu, a nie tam, gdzie jest przydzielany, za pomocą funkcji currentValueOf.

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

W tym przykładzie obserwuje się wartość zmiennej LocalContentColor, która pozwala narysować tło na podstawie jego koloru. Ponieważ ContentDrawScope obserwuje zmiany w zrzutach, automatycznie jest ono ponownie pobierane, 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 spoza zakresu i automatycznie aktualizować modyfikator, użyj ObserverModifierNode.

Na przykład Modifier.scrollable używa tej metody, aby obserwować zmiany w elemencie 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. Umożliwi to korzystanie z interfejsów Compose Animatable API. Na przykład ten fragment kodu zmienia z góry element CircleNode tak, by 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. Jest wiele przypadków użycia tego rozwiązania, np. wyodrębnianie wspólnych implementacji z różnymi modyfikatorami. Można go też wykorzystać do wyświetlania wspólnego stanu między modyfikatorami.

Oto podstawowa implementacja klikalnego węzła modyfikatora, który udostępnia dane o 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ły są unieważniane automatycznie po zaktualizowaniu odpowiednich wywołań ModifierNodeElement. Czasami w bardziej złożonym modyfikatorze możesz zrezygnować z tego zachowania, aby mieć dokładniejszą kontrolę nad tym, kiedy modyfikator unieważnia fazy.

Jest to szczególnie przydatne, gdy niestandardowy modyfikator modyfikuje zarówno układ, jak i rysowanie. Rezygnacja z automatycznej dezaktywacji pozwala unieważnić rysowanie tylko wtedy, gdy właściwości związane z rysowaniem, takie jak color, zmieniają się i nie unieważniają układu. Może to poprawić skuteczność modyfikatora.

Poniżej znajduje się hipotetyczny przykład tego stwierdzenia z modyfikatorem, którego właściwości lambda to color, size i onClick. Unieważnia on tylko to, co jest wymagane, i pomija te 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)
        }
    }
}