Tworzenie modyfikatorów niestandardowych

Compose udostępnia wiele modyfikatorów dostępnych od razu po zainstalowaniu, ale możesz też tworzyć własne modyfikatory niestandardowe.

Modyfikatory mają kilka części:

  • Modyfikator fabryczny
    • Jest to funkcja rozszerzenia w Modifier, która udostępnia idiomatyczny interfejs API dla modyfikatora i umożliwia łatwe łańcuchowe łączenie modyfikatorów. Fabryka modyfikatorów generuje elementy modyfikatorów używane przez Compose do modyfikowania interfejsu.
  • Element modyfikujący
    • Tutaj możesz zaimplementować działanie modyfikatora.

W zależności od potrzebnej funkcjonalności modyfikator niestandardowy można zaimplementować na kilka sposobów. Często najłatwiejszym sposobem wdrożenia niestandardowego modyfikatora jest wdrożenie niestandardowej fabryki modyfikatorów, która łączy ze sobą inne zdefiniowane już fabryki modyfikatorów. Jeśli potrzebujesz bardziej niestandardowego zachowania, zaimplementuj element modifier za pomocą interfejsów API Modifier.Node, które są interfejsami niższego poziomu, ale zapewniają większą elastyczność.

Łączenie dotychczasowych modyfikatorów

Często można tworzyć modyfikatory niestandardowe, korzystając tylko z dotychczasowych modyfikatorów. Na przykład Modifier.clip() jest implementowane za pomocą modyfikatora graphicsLayer. Ta strategia korzysta z dotychczasowych elementów modyfikatora, a Ty musisz podać własną fabrykę modyfikatorów niestandardowych.

Zanim wdrożysz własny modyfikator niestandardowy, sprawdź, czy nie możesz użyć tej samej strategii.

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

Jeśli zauważysz, że często powtarzasz tę samą grupę modyfikatorów, możesz je umieścić w ramach własnego modyfikatora:

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

Tworzenie niestandardowego modyfikatora za pomocą fabryki modyfikatorów złożonych

Możesz też utworzyć niestandardowy modyfikator, używając funkcji składanej, aby przekazywać wartości do istniejącego modyfikatora. Jest to tzw. fabryka modyfikatorów składanych.

Korzystanie z fabryki modyfikatorów składanych do tworzenia modyfikatorów umożliwia też używanie interfejsów API do tworzenia złożonych elementów na wyższym poziomie, takich jak animate*AsState i inne interfejsy API do tworzenia animacji na podstawie stanu. Na przykład ten fragment kodu pokazuje modyfikator, który animuje zmianę wartości 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 wygodną metodą na dostarczanie wartości domyślnych z poziomu CompositionLocal, najprostszym sposobem na wdrożenie tego rozwiązania jest użycie modułowej fabryki modyfikatorów:

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

Takie podejście ma jednak pewne ograniczenia, które opisujemy poniżej.

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

Podczas tworzenia modyfikatora niestandardowego za pomocą fabryki modyfikatorów kompozytowych kompozycja lokalna pobiera wartość z drzewa kompozycji, w którym jest tworzona, a nie z drzewa kompozycji, w którym jest używana. Może to prowadzić do nieoczekiwanych wyników. Weźmy na przykład kompozycję z lokalnym modyfikatorem z powyższego przykładu, która jest nieco inaczej zaimplementowana za pomocą funkcji kompozytowej:

@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 taki sposób, użyj zamiast tego modyfikatora niestandardowego Modifier.Node, ponieważ lokalne elementy kompozycji zostaną poprawnie rozwiązane w miejscu użycia i można je bezpiecznie podnosić.

Modyfikatory funkcji typu „composable” nigdy nie są pomijane.

Modyfikatory fabryk składanych nigdy nie są pominane, ponieważ składane funkcje, które zwracają wartości, nie mogą być pomijane. Oznacza to, że funkcja modyfikatora będzie wywoływana przy każdym przekształceniu, co może być kosztowne, jeśli będzie się ono często powtarzać.

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

Podobnie jak wszystkie funkcje składane, modyfikator fabryki składanej musi być wywoływany z poziomu kompozycji. Ogranicza to miejsce, do którego może zostać przeniesiony modyfikator, ponieważ nigdy nie może zostać przeniesiony poza kompozycję. Z kolei fabryki modyfikatorów, które nie są kompozytowe, można wyodrębnić z funkcji kompozytowych, 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
}

Implementowanie działania niestandardowego 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, który Compose wykorzystuje do implementowania własnych modyfikatorów. Jest to też najbardziej wydajny sposób tworzenia niestandardowych modyfikatorów.

Implementowanie niestandardowego modyfikatora za pomocą Modifier.Node

Implementacja niestandardowego modyfikatora za pomocą węzła 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ła modyfikatora.
  • Opcjonalna fabryka modyfikatorów opisana powyżej.

Klasy ModifierNodeElement są stanowe i przy każdej rekompozycji przydzielane są nowe instancje, podczas gdy klasy Modifier.Node mogą być stanowe i przetrwają przez wiele rekompozycji, a nawet mogą być ponownie użyte.

W następnej sekcji 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) realizuje funkcjonalność niestandardowego modyfikatora.

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

W tym przykładzie rysuje okrąg o kolorze przekazanym funkcji modyfikatora.

Węzeł implementuje Modifier.Node oraz co najmniej 0 typów węzłów. Istnieją różne typy węzłów, które zależą od funkcji wymaganych przez modyfikator. Przykład powyżej musi mieć możliwość rysowania, więc implementuje DrawModifierNode, co pozwala mu zastąpić metodę draw().

Dostępne typy:

Węzeł

Wykorzystanie

Link do przykładu

LayoutModifierNode

Modifier.Node, który zmienia sposób pomiaru i układ zawiniętych treści.

Przykład

DrawModifierNode

Modifier.Node, który wypełnia przestrzeń układu.

Przykład

CompositionLocalConsumerModifierNode

Dzięki wdrożeniu tego interfejsu aplikacja Modifier.Node może odczytywać lokalne elementy kompozycji.

Przykład

SemanticsModifierNode

Modifier.Node, który dodaje pary klucz-wartość do użycia w celu testowania, ułatwiania dostępu i podobnych zastosowań.

Przykład

PointerInputModifierNode

Modifier.Node, który odbiera zdarzenie PointerInputChanges.

Przykład

ParentDataModifierNode

Modifier.Node, który przekazuje dane do szablonu nadrzędnego.

Przykład

LayoutAwareModifierNode

Modifier.Node, który odbiera wywołania zwrotne onMeasuredonPlaced.

Przykład

GlobalPositionAwareModifierNode

Modifier.Node, który otrzymuje wywołanie zwrotne onGloballyPositioned z ostatecznym LayoutCoordinates układu, gdy globalna pozycja treści może się zmienić.

Przykład

ObserverModifierNode

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

Przykład

DelegatingNode

Modifier.Node, który może delegować zadania do innych instancji Modifier.Node.

Może to być przydatne, gdy chcesz połączyć kilka implementacji węzłów w jedną.

Przykład

TraversableNode

Umożliwia elementom klasy Modifier.Node przechodzenie w górę i w dół po drzewie węzłów w przypadku elementów klasy tego samego typu lub określonego klucza.

Przykład

Po wywołaniu metody update na odpowiednim elemencie węzły są automatycznie unieważniane. Ponieważ nasz przykład to DrawModifierNode, każda aktualizacja wywoływana w tym elemencie powoduje ponowne narysowanie węzła i poprawne zaktualizowanie jego koloru. Możesz zrezygnować z automatycznego unieważniania, jak opisano poniżej.

ModifierNodeElement

ModifierNodeElement to niezmienna klasa, która przechowuje dane potrzebne 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ąpić te metody:

  1. create: to funkcja, która tworzy instancję węzła modyfikatora. Jest on wywoływany, aby utworzyć węzeł, gdy modyfikator zostanie po raz pierwszy zastosowany. Zwykle oznacza to tworzenie węzła i konfigurowanie go za pomocą parametrów przekazanych fabryce modyfikatorów.
  2. update: ta funkcja jest wywoływana, gdy ten modyfikator jest podany w tym samym miejscu, w którym węzeł już istnieje, ale zmieniła się jego właściwość. Zależy to od metody equals klasy. Utworzony wcześniej węzeł modyfikatora jest wysyłany jako parametr do wywołania update. W tym momencie powinieneś zaktualizować właściwości węzłów, aby odpowiadały one zaktualizowanym parametrom. Możliwość ponownego użycia węzłów w ten sposób jest kluczowa dla zwiększenia wydajności, jakie zapewnia Modifier.Node. Dlatego musisz zaktualizować istniejący węzeł, a nie tworzyć nowego za pomocą metody update. W naszym przykładzie kółko zmienia kolor.

Dodatkowo implementacje ModifierNodeElement muszą też implementować equalshashCode. update zostanie wywołany tylko wtedy, gdy porównanie z poprzednim elementem zwróci wartość fałsz.

W przykładzie powyżej 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ł musi zostać zaktualizowany, lub jeśli chcesz uniknąć klas danych ze względu na zgodność binarną, możesz ręcznie zaimplementować equals i hashCode, np. element modyfikatora wypełnienia.

Fabryka modyfikatorów

To jest publiczna strona interfejsu API modyfikatora. Większość implementacji po prostu tworzy 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 tworzą niestandardowy modyfikator do rysowania koła 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żywane są elementy Modifier.Node

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

Zero parametrów

Jeśli modyfikator nie ma parametrów, nigdy nie trzeba go aktualizować, a dodatkowo nie musi być klasą danych. Oto przykładowa implementacja modyfikatora, który stosuje stały odstęp do kompozytowalności:

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ływanie się do lokalizacji w kompozycji

Modyfikatory Modifier.Node nie rejestrują automatycznie zmian stanu obiektów Compose, takich jak CompositionLocal. Zaletą modyfikatorów Modifier.Node w porównaniu z modyfikatorami utworzonymi za pomocą fabryki kompozytowej jest to, że mogą one odczytywać wartość kompozycji lokalnej z miejsca, w którym modyfikator jest używany w drzewie interfejsu użytkownika, a nie z miejsca, w którym jest przypisany, za pomocą currentValueOf.

Jednak instancje węzła modyfikatora nie rejestrują automatycznie zmian stanu. Aby automatycznie reagować na zmiany w kompozycji lokalnej, 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 reaguje na zmiany w migawkach, 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 funkcja Modifier.scrollable używa tej techniki do obserwowania zmian w funkcji LocalDensity. Poniżej przedstawiamy 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 animacji

Implementacje Modifier.Node mają dostęp do zasobu coroutineScope. Pozwala to na korzystanie z interfejsów Compose Animatable API. Na przykład ten fragment kodu modyfikuje CircleNode z powyższego przykładu, aby efekt płynnego pojawiania się i znikania był powtarzany:

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 za pomocą delegowania

Modifier.Node modyfikatory mogą delegować do innych węzłów. Ma ono wiele zastosowań, takich jak wyodrębnianie wspólnych implementacji w różnych modyfikatorach, ale może też służyć do udostępniania wspólnego stanu w różnych modyfikatorach.

Oto przykład podstawowej implementacji 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ła

Modifier.Node są automatycznie nieważne, gdy ich odpowiadające im wywołania ModifierNodeElement zostaną zaktualizowane. Czasami w przypadku bardziej złożonego modyfikatora możesz chcieć zrezygnować z tego zachowania, aby uzyskać bardziej szczegółową kontrolę nad tym, kiedy modyfikator unieważnia fazy.

Może to być szczególnie przydatne, jeśli modyfikator niestandardowy modyfikuje zarówno układ, jak i rysowanie. Wyłączenie automatycznego unieważniania pozwala unieważnić tylko draw, gdy zmieniają się tylko właściwości związane z draw, np. color, a nie unieważniać układu. Może to zwiększyć skuteczność modyfikatora.

Poniżej przedstawiamy hipotetyczny przykład modyfikatora z właściwościami color, size i lambda onClick. Ten modyfikator unieważnia tylko to, co jest wymagane, i pomija wszelkie nieprawidłowe elementy:

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