Criar modificadores personalizados

O Compose oferece muitos modificadores para comportamentos comuns prontos para uso, mas você também pode criar modificadores personalizados.

Os modificadores têm várias partes:

  • Uma fábrica de modificadores
    • Essa é uma função de extensão no Modifier, que fornece uma API idiomática para seu modificador e permite que eles sejam facilmente encadeados. O a fábrica de modificadores produz os elementos modificadores usados pelo Compose para modificar sua interface.
  • Um elemento modificador
    • É aqui que você pode implementar o comportamento do seu modificador.

Há várias maneiras de implementar um modificador personalizado, dependendo do a funcionalidade necessária. Muitas vezes, a maneira mais fácil de implementar um modificador personalizado é apenas para implementar uma fábrica de modificadores personalizados, que combina outros elementos já definidos e fábricas de modificadores. Se você precisar de mais comportamento personalizado, implemente o elemento modificador usando as APIs Modifier.Node, que são de nível inferior, mas e ter mais flexibilidade.

Encadear modificadores atuais

Muitas vezes, é possível criar modificadores personalizados usando apenas modificadores. Por exemplo, Modifier.clip() é implementado usando o Modificador graphicsLayer. Essa estratégia usa elementos modificadores existentes, e você fornecer sua própria fábrica de modificadores personalizados.

Antes de implementar seu próprio modificador personalizado, veja se você pode usar o mesmo estratégia.

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

Ou, se você achar que está repetindo o mesmo grupo de modificadores com frequência, poderá envolva-os no seu próprio modificador:

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

Criar um modificador personalizado usando uma fábrica de modificadores combináveis

Também é possível criar um modificador personalizado usando uma função combinável para transmitir valores a um modificador atual. Isso é conhecido como fábrica de modificadores combináveis.

O uso de uma fábrica de modificadores combináveis para criar um modificador também permite o uso de APIs do Compose de nível mais alto, como animate*AsState e outras APIs do Compose APIs de animação com estado. Por exemplo, o snippet a seguir mostra modificador que anima uma mudança alfa quando ativado/desativado:

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

Se seu modificador personalizado for um método de conveniência para fornecer valores padrão de uma CompositionLocal, a maneira mais fácil de implementar isso é usando um elemento combinável. fábrica de modificadores:

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

Essa abordagem tem algumas ressalvas detalhadas abaixo.

Os valores de CompositionLocal são resolvidos no local de chamada da fábrica de modificadores

Ao criar um modificador personalizado usando uma fábrica de modificadores combináveis, locais pegam o valor da árvore de composição onde são criados, não usados. Isso pode levar a resultados inesperados. Por exemplo, pegue a composição exemplo de modificador local acima, implementado de forma um pouco diferente usando um função combinável:

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

Se não for assim que você espera que seu modificador funcione, use um modificador Modifier.Node, já que os locais de composição serão resolvido corretamente no local de uso e pode ser elevado com segurança.

Os modificadores de função combinável nunca são pulados

Modificadores de fábrica combináveis nunca são ignorados porque as funções combináveis que têm valores de retorno não podem ser pulados. Isso significa que sua função modificadora será chamado em cada recomposição, o que pode ser caro se ela for recomposta com frequência.

Modificadores de funções combináveis precisam ser chamados dentro de uma função combinável

Como todas as funções combináveis, um modificador de fábrica combinável precisa ser chamado de na composição. Isso limita onde um modificador pode ser elevado, o que pode nunca serão elevados para fora da composição. Em comparação, o modificador não combinável fábricas podem ser elevadas de funções combináveis para facilitar a reutilização e melhorar o desempenho:

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
}

Implementar o comportamento do modificador personalizado usando Modifier.Node

A Modifier.Node é uma API de nível inferior para criar modificadores no Compose. Ela é a mesma API em que o Compose implementa os próprios modificadores e é a mais indicada. eficiente para criar modificadores personalizados.

Implementar um modificador personalizado usando Modifier.Node

A implementação de um modificador personalizado usando Modifier.Node se divide em três partes:

  • Uma implementação de Modifier.Node que contém a lógica e estado do modificador.
  • Um ModifierNodeElement que cria e atualiza um modificador instâncias de nó.
  • Uma fábrica de modificadores opcional, conforme detalhado acima.

As classes ModifierNodeElement não têm estado, e as novas instâncias são alocadas recomposição, enquanto as classes Modifier.Node podem ter estado e sobreviver em várias recomposições e podem até ser reutilizadas.

A seção a seguir descreve cada parte e mostra um exemplo de como criar um personalizado para desenhar um círculo.

Modifier.Node

A implementação de Modifier.Node (neste exemplo, CircleNode) implementa as do modificador personalizado.

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

Neste exemplo, o círculo é desenhado com a cor transmitida ao modificador função.

Um nó implementa Modifier.Node, bem como zero ou mais tipos de nó. Existem diferentes tipos de nó com base na funcionalidade necessária para seu modificador. O exemplo acima precisa ser capaz de desenhar, por isso implementa DrawModifierNode, que permite que ele substitua o método de desenho.

Os tipos disponíveis são os seguintes:

Uso

Link de exemplo

LayoutModifierNode

Um Modifier.Node que muda a forma como o conteúdo encapsulado é medido e disposto.

Exemplo

DrawModifierNode

Um Modifier.Node que é desenhado no espaço do layout.

Exemplo

CompositionLocalConsumerModifierNode

A implementação dessa interface permite que o Modifier.Node leia os locais de composição.

Exemplo

SemanticsModifierNode

Um Modifier.Node que adiciona chave-valor de semântica para uso em testes, acessibilidade e casos de uso semelhantes.

Exemplo

PointerInputModifierNode

Uma Modifier.Node que recebe PointerInputChanges.

Exemplo

ParentDataModifierNode

Uma Modifier.Node que fornece dados para o layout pai.

Exemplo

LayoutAwareModifierNode

Um Modifier.Node que recebe callbacks onMeasured e onPlaced.

Exemplo

GlobalPositionAwareModifierNode

Um Modifier.Node que recebe um callback onGloballyPositioned com o LayoutCoordinates final do layout quando a posição global do conteúdo pode ter mudado.

Exemplo

ObserverModifierNode

Modifier.Nodes que implementam ObserverNode podem fornecer a própria implementação de onObservedReadsChanged que será chamada em resposta a mudanças em objetos de snapshot lidos em um bloco observeReads.

Exemplo

DelegatingNode

Uma Modifier.Node que pode delegar trabalho a outras instâncias da Modifier.Node.

Isso pode ser útil para compor várias implementações de nó em uma.

Exemplo

TraversableNode

Permite que as classes Modifier.Node percorram a árvore de nós para cima/baixo em busca de classes do mesmo tipo ou de uma chave específica.

Exemplo

Os nós são automaticamente invalidados quando a atualização é chamada no . Como nosso exemplo é uma DrawModifierNode, toda vez que uma atualização de horário é chamada elemento, o nó aciona um redesenho e sua cor é atualizada corretamente. É possível desativar a invalidação automática, conforme detalhado abaixo.

ModifierNodeElement

Um ModifierNodeElement é uma classe imutável que contém os dados para criar ou atualize seu modificador personalizado:

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

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

As implementações de ModifierNodeElement precisam substituir os seguintes métodos:

  1. create: é a função que instancia o nó modificador. Isso gera chamado para criar o nó quando seu modificador for aplicado pela primeira vez. Normalmente, isso para construir o nó e configurá-lo com os parâmetros que foram transmitidos para a fábrica de modificadores.
  2. update: essa função é chamada sempre que esse modificador é fornecido no mesmo ponto em que este nó já existe, mas uma propriedade foi alterada. Isso é determinado pelo método equals da classe. O nó modificador que foi criada anteriormente é enviada como um parâmetro para a chamada update. Neste ponto, você deve atualizar para corresponder à versão atualizada parâmetros. A capacidade de reutilizar os nós dessa forma é fundamental para de performance que Modifier.Node traz portanto, você deve atualizar nó atual em vez de criar um novo no método update. Em nossa exemplo de círculo, a cor do nó será atualizada.

Além disso, as implementações de ModifierNodeElement também precisam implementar equals. e hashCode. update só será chamado se uma comparação de igualdade com o elemento anterior retorna falso.

O exemplo acima usa uma classe de dados para fazer isso. Esses métodos são usados para verifica se um nó precisa ser atualizado ou não. Se o elemento tiver propriedades que não contribuem para a necessidade de atualização de um nó ou para evitar a por motivos de compatibilidade binária, é possível implementar equals manualmente e hashCode. Por exemplo: o elemento modificador de padding.

Fábrica de modificadores

Esta é a superfície da API pública do seu modificador. A maioria das implementações simplesmente crie o elemento modificador e o adicione à cadeia de modificadores:

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

Exemplo completo

Essas três partes se unem para criar o modificador personalizado para desenhar um círculo usando as APIs 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)
    }
}

Situações comuns com o uso de Modifier.Node

Ao criar modificadores personalizados com Modifier.Node, veja algumas situações comuns que você pode encontrarem.

Nenhum parâmetro

Se o modificador não tiver parâmetros, ele nunca vai precisar ser atualizado. e, além disso, não precisa ser uma classe de dados. Este é um exemplo de implementação de um modificador que aplica uma quantidade fixa de padding a um elemento combinável:

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

Referência a locais de composição

Os modificadores Modifier.Node não observam automaticamente as mudanças no estado do Compose. objetos, como CompositionLocal. A vantagem dos modificadores Modifier.Node em relação a ou modificadores recém-criados com uma fábrica combinável é que eles podem ler O valor do local de composição em que o modificador é usado na interface árvore, não onde o modificador está alocado, usando currentValueOf.

No entanto, as instâncias de nó modificador não observam automaticamente as mudanças de estado. Para automaticamente a uma mudança local de composição, você pode ler a dentro de um escopo:

Este exemplo observa o valor de LocalContentColor para desenhar um plano de fundo com base na sua cor. Como ContentDrawScope observa mudanças no snapshot, esse é redesenhado automaticamente quando o valor de LocalContentColor muda:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Para reagir a mudanças de estado fora de um escopo e atualizar automaticamente seu , use um ObserverModifierNode.

Por exemplo, o Modifier.scrollable usa essa técnica para observar mudanças em LocalDensity. Confira abaixo um exemplo simplificado:

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

Modificador de animação

As implementações de Modifier.Node têm acesso a um coroutineScope. Isso permite uso das APIs Compose Animatable. Por exemplo, este snippet modifica a CircleNode acima para aparecer e desaparecer repetidamente:

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

Como compartilhar estado entre modificadores usando delegação

Os modificadores Modifier.Node podem delegar a outros nós. Há muitos casos de uso para isso, como a extração de implementações comuns em diferentes modificadores, mas também pode ser usado para compartilhar um estado comum entre modificadores.

Por exemplo, uma implementação básica de um nó modificador clicável que compartilha dados de interação:

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

Como desativar a invalidação automática do nó

Modifier.Node nós são invalidados automaticamente quando seus Atualização de chamadas de ModifierNodeElement. Às vezes, em um modificador mais complexo, desativar esse comportamento para ter um controle mais preciso sobre quando o modificador invalida fases.

Isso pode ser útil se seu modificador personalizado modificar o layout e empate. Se você desativar a invalidação automática, só será possível invalidar o desenho quando apenas propriedades relacionadas a desenhos, como color, mudam, e não invalidam o layout. Isso pode melhorar o desempenho do seu modificador.

Um exemplo hipotético disso é mostrado abaixo com um modificador que tem um color. lambda size e onClick como propriedades. Esse modificador só invalida o que é obrigatório e ignora qualquer invalidação que não seja:

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