Migrar para as APIs Indication e Ripple

Para melhorar o desempenho de composição de componentes interativos que usam Modifier.clickable, introduzimos novas APIs. Essas APIs permitem implementações mais eficientes de Indication, como ondulações.

androidx.compose.foundation:foundation:1.7.0+ e androidx.compose.material:material-ripple:1.7.0+ incluem as seguintes mudanças na API:

Suspenso

Substituição

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Novas APIs ripple() fornecidas nas bibliotecas do Material Design.

Observação: neste contexto, "bibliotecas do Material Design" se refere a androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material e androidx.wear.compose:compose-material3..

RippleTheme

Siga uma destas instruções:

  • Use as APIs RippleConfiguration da biblioteca do Material Design ou
  • Criar sua própria implementação de ondulação do sistema de design

Nesta página, descrevemos o impacto da mudança de comportamento e as instruções para migrar para as novas APIs.

Mudança de comportamento

As seguintes versões da biblioteca incluem uma mudança no comportamento de ondulação:

  • androidx.compose.material:material:1.7.0+
  • androidx.compose.material3:material3:1.3.0+
  • androidx.wear.compose:compose-material:1.4.0+

Essas versões das bibliotecas do Material Design não usam mais rememberRipple(). Em vez disso, elas usam as novas APIs de ondulação. Como resultado, eles não consultam LocalRippleTheme. Portanto, se você definir LocalRippleTheme no aplicativo, os componentes do Material Design não vão usar esses valores.

As seções a seguir descrevem como migrar para as novas APIs.

Migrar de rememberRipple para ripple

Como usar uma biblioteca do Material Design

Se você estiver usando uma biblioteca do Material Design, substitua diretamente rememberRipple() por uma chamada para ripple() da biblioteca correspondente. Essa API cria uma ondulação usando valores derivados das APIs de tema do Material Design. Em seguida, transmita o objeto retornado para Modifier.clickable e/ou outros componentes.

Por exemplo, o snippet a seguir usa as APIs descontinuadas:

Box(
    Modifier.clickable(
        onClick = {},
        interactionSource = remember { MutableInteractionSource() },
        indication = rememberRipple()
    )
) {
    // ...
}

Modifique o snippet acima para:

@Composable
private fun RippleExample() {
    Box(
        Modifier.clickable(
            onClick = {},
            interactionSource = remember { MutableInteractionSource() },
            indication = ripple()
        )
    ) {
        // ...
    }
}

Observe que ripple() não é mais uma função combinável e não precisa ser lembrada. Ele também pode ser reutilizado em vários componentes, de forma semelhante aos modificadores. Portanto, considere extrair a criação de ondulação para um valor de nível superior para economizar alocações.

Como implementar um sistema de design personalizado

Se você estiver implementando seu próprio sistema de design e usava rememberRipple() com um RippleTheme personalizado para configurar o efeito, forneça sua própria API de efeito que delega às APIs de nó de efeito expostas em material-ripple. Em seguida, os componentes podem usar seu próprio efeito de ondulação, que consome os valores do tema diretamente. Para mais informações, consulte Migrar de RippleTheme.

Migrar de RippleTheme

Como usar RippleTheme para desativar uma ondulação em um determinado componente

As bibliotecas material e material3 expõem RippleConfiguration e LocalRippleConfiguration, que permitem configurar a aparência de ondulações em uma subárvore. RippleConfiguration e LocalRippleConfiguration são destinados apenas à personalização por componente. A personalização global/em todo o tema não é compatível com essas APIs. Consulte Como usar RippleTheme para mudar globalmente todas as ondulações em um aplicativo para mais informações sobre esse caso de uso.

Por exemplo, o snippet a seguir usa as APIs descontinuadas:

private object DisabledRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Transparent

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) {
        Button {
            // ...
        }
    }

Modifique o snippet acima para:

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    Button {
        // ...
    }
}

Como usar RippleTheme para mudar a cor/alfa de um efeito de ondulação em um determinado componente

Como descrito na seção anterior, RippleConfiguration e LocalRippleConfiguration são destinados apenas à personalização por componente.

Por exemplo, o snippet a seguir usa as APIs descontinuadas:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Red

    @Composable
    override fun rippleAlpha(): RippleAlpha = MyRippleAlpha
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) {
        Button {
            // ...
        }
    }

Modifique o snippet acima para:

@OptIn(ExperimentalMaterialApi::class)
private val MyRippleConfiguration =
    RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) {
        Button {
            // ...
        }
    }

Como usar RippleTheme para mudar globalmente todas as ondulações em um aplicativo

Antes, era possível usar LocalRippleTheme para definir o comportamento de ondulação em um nível de tema. Esse era essencialmente um ponto de integração entre os locais de composição do sistema de design personalizado e o ripple. Em vez de expor uma primitiva de tema genérica, material-ripple agora expõe uma função createRippleModifierNode(). Essa função permite que as bibliotecas do sistema de design criem uma implementação de wrapper de ordem mais alta, que consulta os valores do tema e delega a implementação de ondulação ao nó criado por essa função.

Isso permite que os sistemas de design consultem diretamente o que precisam e exponham as camadas de temas configuráveis pelo usuário necessárias sem precisar se adequar ao que é fornecido na camada material-ripple. Essa mudança também torna mais explícito o tema/especificação que o efeito de ondulação está seguindo, já que é a própria API de ondulação que define esse contrato, em vez de ser derivado implicitamente do tema.

Para receber orientações, consulte a implementação da API Ripple nas bibliotecas do Material Design e substitua as chamadas para os locais de composição do Material Design conforme necessário para seu próprio sistema de design.

Migrar de Indication para IndicationNodeFactory

Passando por Indication

Se você estiver apenas criando um Indication para transmitir, como criar um efeito de ondulação para transmitir para Modifier.clickable ou Modifier.indication, não será necessário fazer mudanças. IndicationNodeFactory herda de Indication, então tudo vai continuar sendo compilado e funcionando.

Criando Indication

Se você estiver criando sua própria implementação de Indication, a migração será simples na maioria dos casos. Por exemplo, considere um Indication que aplica um efeito de escala ao pressionar:

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

É possível migrar isso em duas etapas:

  1. Migre ScaleIndicationInstance para ser um DrawModifierNode. A superfície da API para DrawModifierNode é muito semelhante à IndicationInstance: ela expõe uma função ContentDrawScope#draw() que é funcionalmente equivalente a IndicationInstance#drawContent(). Você precisa mudar essa função e, em seguida, implementar a lógica collectLatest diretamente no nó, em vez de Indication.

    Por exemplo, o snippet a seguir usa as APIs descontinuadas:

    private class ScaleIndicationInstance : IndicationInstance {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun ContentDrawScope.drawIndication() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@drawIndication.drawContent()
            }
        }
    }

    Modifique o snippet acima para:

    private class ScaleIndicationNode(
        private val interactionSource: InteractionSource
    ) : Modifier.Node(), DrawModifierNode {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. Migre ScaleIndication para implementar IndicationNodeFactory. Como a lógica de coleta agora é movida para o nó, esse é um objeto de fábrica muito simples, cuja única responsabilidade é criar uma instância de nó.

    Por exemplo, o snippet a seguir usa as APIs descontinuadas:

    object ScaleIndication : Indication {
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            // key the remember against interactionSource, so if it changes we create a new instance
            val instance = remember(interactionSource) { ScaleIndicationInstance() }
    
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> instance.animateToResting()
                        is PressInteraction.Cancel -> instance.animateToResting()
                    }
                }
            }
    
            return instance
        }
    }

    Modifique o snippet acima para:

    object ScaleIndicationNodeFactory : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleIndicationNode(interactionSource)
        }
    
        override fun hashCode(): Int = -1
    
        override fun equals(other: Any?) = other === this
    }

Como usar Indication para criar um IndicationInstance

Na maioria dos casos, use Modifier.indication para exibir Indication para um componente. No entanto, no caso raro de você criar manualmente um IndicationInstance usando rememberUpdatedInstance, será necessário atualizar sua implementação para verificar se o Indication é um IndicationNodeFactory para que você possa usar uma implementação mais leve. Por exemplo, Modifier.indication vai delegar internamente ao nó criado se ele for um IndicationNodeFactory. Caso contrário, ele usará Modifier.composed para chamar rememberUpdatedInstance.