O Compose oferece muitos modificadores para comportamentos comuns, mas você também pode criar seus próprios modificadores personalizados.
Os modificadores têm várias partes:
- Uma fábrica de modificadores
- Essa é uma função de extensão em
Modifier, que fornece uma API idiomática para o modificador e permite que os modificadores sejam encadeados. A fábrica de modificadores produz os elementos modificadores usados pelo Compose para modificar sua IU.
- Essa é uma função de extensão em
- Um elemento modificador
- É aqui que você pode implementar o comportamento do modificador.
Há várias maneiras de implementar um modificador personalizado, dependendo da
funcionalidade necessária. Muitas vezes, a maneira mais simples de implementar um modificador personalizado é
implementar uma fábrica de modificadores personalizada que combine outras fábricas de modificadores
já definidas. Se você precisar de um comportamento mais personalizado, implemente o elemento
modificador usando as APIs Modifier.Node, que são de nível mais baixo, mas oferecem mais
flexibilidade.
Encadear modificadores existentes
Muitas vezes, é possível criar modificadores personalizados usando os que já existem. Por
exemplo, Modifier.clip() é implementado usando o modificador
graphicsLayer. Essa estratégia usa elementos de modificador existentes, e você fornece sua
própria fábrica de modificadores personalizados.
Antes de implementar seu próprio modificador personalizado, verifique se é possível usar a mesma estratégia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Ou, se você perceber que está repetindo o mesmo grupo de modificadores com frequência, pode agrupá-los 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 uma fábrica de modificadores combináveis.
Usar uma fábrica de modificadores combináveis para criar um modificador também permite usar
APIs do Compose de nível mais alto, como animate*AsState e outras APIs de animação
com suporte de estado do Compose. Por exemplo, o snippet abaixo mostra um
modificador que anima uma mudança de 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 o modificador personalizado for um método de conveniência para fornecer valores padrão de um
CompositionLocal, a maneira mais fácil de implementar isso é usar uma fábrica de modificadores
combinável:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Essa abordagem tem algumas ressalvas, que são detalhadas nas seções a seguir.
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, os locais de composição recebem o valor da árvore de composição em que são criados, não usados. Isso pode levar a resultados inesperados. Por exemplo, considere o exemplo de modificador de composição local mencionado anteriormente, implementado de forma um pouco diferente usando uma 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 é assim que você espera que o modificador funcione, use um
Modifier.Node personalizado, já que os locais de composição serão
resolvidos corretamente no site de uso e poderão ser elevados com segurança.
Os modificadores de funções combináveis nunca são ignorados
Os modificadores de fábrica combináveis nunca são pulados porque as funções combináveis que têm valores de retorno não podem ser puladas. Isso significa que a função do modificador será chamada em cada recomposição, o que pode ser caro se ela for recomposta com frequência.
Os modificadores de função combinável 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 dentro da composição. Isso limita onde um modificador pode ser elevado, já que ele nunca pode ser elevado para fora da composição. Em comparação, as fábricas de modificadores não combináveis podem ser elevadas para fora das 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
Modifier.Node é uma API de nível mais baixo para criar modificadores no Compose. É
a mesma API que o Compose usa para implementar os próprios modificadores e é a maneira mais
eficiente de criar modificadores personalizados.
Implementar um modificador personalizado usando Modifier.Node
Há três partes para implementar um modificador personalizado usando Modifier.Node:
- Uma implementação de
Modifier.Nodeque contém a lógica e o estado do modificador. - Um
ModifierNodeElementque cria e atualiza instâncias de nó modificador. - Uma fábrica de modificadores opcional, conforme detalhado anteriormente.
As classes ModifierNodeElement não têm estado, e novas instâncias são alocadas a cada
recomposição, enquanto as classes Modifier.Node podem ter estado, sobreviver
a várias recomposições e até mesmo ser reutilizadas.
A seção a seguir descreve cada parte e mostra um exemplo de como criar um modificador personalizado para desenhar um círculo.
Modifier.Node
A implementação de Modifier.Node (neste exemplo, CircleNode) implementa
a funcionalidade do modificador personalizado.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Neste exemplo, ele desenha o círculo com a cor transmitida para a função do modificador.
Um nó implementa Modifier.Node, além de zero ou mais tipos de nó. Há
diferentes tipos de nós com base na funcionalidade que o modificador exige. O
exemplo anterior precisa ser capaz de desenhar, então ele implementa DrawModifierNode,
o que permite substituir o método de desenho.
Os tipos disponíveis são os seguintes:
Nó |
Uso |
Link de exemplo |
Um |
||
Um |
||
A implementação dessa interface permite que o |
||
Um |
||
Um |
||
Um |
||
Um |
||
Um |
||
As |
||
Um Isso pode ser útil para compor várias implementações de nós em uma só. |
||
Permite que as classes |
Os nós são invalidados automaticamente quando a atualização é chamada no elemento
correspondente. Como nosso exemplo é um DrawModifierNode, sempre que update é chamado
no elemento, o nó aciona uma nova renderização, e a cor é atualizada corretamente. É
possível desativar a invalidação automática, conforme detalhado na seção
Desativar a invalidação automática de nós.
ModifierNodeElement
Um ModifierNodeElement é uma classe imutável que contém os dados para criar ou
atualizar o 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:
create: é a função que instancia o nó do modificador. Ele é chamado para criar o nó quando o modificador é aplicado pela primeira vez. Normalmente, isso equivale a construir o nó e configurá-lo com os parâmetros que foram transmitidos para a fábrica de modificadores.update: essa função é chamada sempre que o modificador é fornecido no mesmo local em que o nó já existe, mas uma propriedade foi alterada. Isso é determinado pelo métodoequalsda classe. O nó modificador que foi criado anteriormente é enviado como um parâmetro para a chamadaupdate. Neste ponto, atualize as propriedades dos nós para que correspondam aos parâmetros atualizados. A capacidade de reutilizar nós dessa forma é fundamental para os ganhos de desempenho que oModifier.Nodetraz. Portanto, é necessário atualizar o nó atual em vez de criar um novo no métodoupdate. No nosso exemplo de círculo, a cor do nó é 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 retornar falso.
O exemplo anterior usa uma classe de dados para fazer isso. Esses métodos são usados
para verificar se um nó precisa ser atualizado ou não. Se o elemento tiver propriedades que
não contribuem para determinar se um nó precisa ser atualizado ou se você quiser evitar
classes de dados por motivos de compatibilidade binária, implemente manualmente
equals e hashCode, por exemplo, o
elemento modificador de padding.
Fábrica de modificadores
Essa é a superfície da API pública do seu modificador. A maioria das implementações cria o elemento modificador e o adiciona à 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 que desenha 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 ao usar Modifier.Node
Ao criar modificadores personalizados com Modifier.Node, estas são algumas situações comuns
que você pode encontrar.
Zero parâmetro
Se o modificador não tiver parâmetros, ele nunca precisará ser atualizado e, além disso, não precisará ser uma classe de dados. Confira abaixo 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) } } }
Referenciar classes Composition Local
Os modificadores Modifier.Node não observam automaticamente as mudanças nos objetos de estado do Compose, como CompositionLocal. A vantagem que os modificadores Modifier.Node têm
sobre os modificadores criados apenas com uma fábrica de elementos combináveis é que eles podem
ler o valor do local de composição de onde o modificador é usado na
árvore de IU, não onde o modificador é alocado, usando currentValueOf.
No entanto, as instâncias de nó modificador não observam automaticamente as mudanças de estado. Para reagir automaticamente a uma mudança de local de composição, leia o valor atual em um escopo:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScopeeIntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
Este exemplo observa o valor de LocalContentColor para desenhar um plano de fundo
com base na cor dele. Como ContentDrawScope observa as mudanças de snapshot, isso
é 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 o
modificador, use um ObserverModifierNode.
Por exemplo, Modifier.scrollable usa essa técnica para
observar mudanças em LocalDensity. Confira um exemplo simplificado
abaixo:
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) } }
Animar um modificador
As implementações de Modifier.Node têm acesso a um coroutineScope. Isso permite
o uso das APIs Animatable do Compose. Por exemplo, este snippet modifica
o CircleNode mostrado anteriormente para aparecer e desaparecer repetidamente:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private lateinit var alpha: Animatable<Float, AnimationVector1D> override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Compartilhar o estado entre modificadores usando delegação
Os modificadores Modifier.Node podem delegar para outros nós. Há muitos casos de uso
para isso, como extrair 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) ) }
Desativar a invalidação automática de nós
Os nós Modifier.Node são invalidados automaticamente quando as chamadas ModifierNodeElement correspondentes são atualizadas. Para modificadores complexos, talvez seja melhor desativar
esse comportamento para ter um controle mais detalhado sobre quando o modificador
invalida as fases.
Isso é muito útil se o modificador personalizado modifica o layout e
o desenho. Desativar a invalidação automática permite que você invalide o desenho apenas quando
as propriedades relacionadas a ele, como color, mudarem. Isso evita a invalidação
do layout e pode melhorar o desempenho do modificador.
Um exemplo hipotético disso é mostrado no exemplo abaixo com um modificador
que tem uma lambda color, size e onClick como propriedades. Esse modificador
invalida apenas o que é necessário, ignorando qualquer invalidação desnecessária:
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) } } }