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.
- Essa é uma função de extensão no
- 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:
Nó |
Uso |
Link de exemplo |
Um |
||
Um |
||
A implementação dessa interface permite que o |
||
Um |
||
Uma |
||
Uma |
||
Um |
||
Um |
||
|
||
Uma Isso pode ser útil para compor várias implementações de nó em uma. |
||
Permite que as classes |
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:
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.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étodoequals
da classe. O nó modificador que foi criada anteriormente é enviada como um parâmetro para a chamadaupdate
. 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 queModifier.Node
traz portanto, você deve atualizar nó atual em vez de criar um novo no métodoupdate
. 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:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
eIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
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) } } }