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 de Indication mais eficientes, como ondulações.

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

Descontinuado

Substituição

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

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

Observação: neste contexto, "Bibliotecas de materiais" se refere a androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material e androidx.wear.compose:compose-material3..

RippleTheme

maneiras:

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

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

Mudança de comportamento

As seguintes versões da biblioteca incluem uma mudança de 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 de bibliotecas do Material Design não usam mais rememberRipple(). Em vez disso, elas usam as novas APIs de ondulação. Como resultado, elas não consultam LocalRippleTheme. Portanto, se você definir LocalRippleTheme no seu aplicativo, os componentes do Material Design não usarão esses valores.

A seção a seguir descreve como voltar temporariamente ao comportamento antigo sem migrar. No entanto, recomendamos migrar para as novas APIs. Para instruções de migração, consulte Migrar de rememberRipple para ripple e as próximas seções.

Fazer upgrade da versão da biblioteca do Material Design sem migrar

Para desbloquear o upgrade de versões da biblioteca, use a API LocalUseFallbackRippleImplementation CompositionLocal temporária para configurar os componentes do Material Design para retornar ao comportamento antigo:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Forneça isso fora do MaterialTheme para que as ondulações antigas possam ser fornecidas pelo LocalIndication.

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 rememberRipple() diretamente 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 lembrado. Ela também pode ser reutilizada em vários componentes, semelhante aos modificadores. Portanto, considere extrair a criação de ondulação para um valor de nível superior para salvar as alocações.

Implementar um sistema de design personalizado

Se você está implementando seu próprio sistema de design e já estava usando rememberRipple() com um RippleTheme personalizado para configurar a ondulação, forneça sua própria API de ondulação que delega para as APIs de nó de ondulação exibidas em material-ripple. Assim, seus componentes podem usar sua própria ondulação que consome os valores do tema diretamente. Para mais informações, consulte Migrar do RippleTheme.

Migrar do RippleTheme

Desativar temporariamente a mudança de comportamento

As bibliotecas do Material Design têm um CompositionLocal temporário, LocalUseFallbackRippleImplementation, que pode ser usado para configurar todos os componentes do Material Design para voltar a usar rememberRipple. Dessa forma, rememberRipple continua consultando LocalRippleTheme.

O snippet de código a seguir demonstra como usar a API LocalUseFallbackRippleImplementation CompositionLocal:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Se você estiver usando um tema personalizado do app criado com base no Material Design, poderá fornecer com segurança a composição local como parte do tema do app:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
        MaterialTheme(content = content)
    }
}

Para ver mais informações, consulte a seção Fazer upgrade da versão da biblioteca do Material Design sem migrar.

Uso de 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 das ondulações em uma subárvore. Observe que RippleConfiguration e LocalRippleConfiguration são experimentais e se destinam apenas à personalização por componente. A personalização global/do tema não tem suporte para 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:

@OptIn(ExperimentalMaterialApi::class)
private val DisabledRippleConfiguration =
    RippleConfiguration(isEnabled = false)

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

Uso de RippleTheme para mudar a cor/alfa de uma ondulação em um determinado componente

Conforme descrito na seção anterior, RippleConfiguration e LocalRippleConfiguration são APIs experimentais e se destinam 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 {
            // ...
        }
    }

Uso de RippleTheme para mudar globalmente todas as ondulações em um app

Anteriormente, era possível usar LocalRippleTheme para definir o comportamento de ondulação em um nível do tema. Esse era essencialmente um ponto de integração entre os locais de composição do sistema de design personalizado e a ondulação. Em vez de expor um primitivo de tema genérico, o 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 superior, que consultam os valores de tema e deleguem a implementação de ondulação ao nó criado por essa função.

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

Para conferir orientações, consulte a implementação da API de ondulação (link em inglês) 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ê está apenas criando um Indication para transmissão, como uma ondulação para transmitir a Modifier.clickable ou Modifier.indication, não é necessário fazer alterações. O IndicationNodeFactory é herdado de Indication, então tudo 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 DrawModifierNode. A superfície da API para DrawModifierNode é muito semelhante a IndicationInstance: ela expõe uma função ContentDrawScope#draw() que é funcionalmente equivalente a IndicationInstance#drawContent(). Você precisa alterar essa função e, em seguida, implementar a lógica collectLatest diretamente dentro do 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 é movida para o nó, esse é um objeto de fábrica muito simples, com a única responsabilidade de 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 raro caso de você estar criando manualmente um IndicationInstance usando rememberUpdatedInstance, é 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 delegará internamente o nó criado se ele for um IndicationNodeFactory. Caso contrário, ele vai usar Modifier.composed para chamar rememberUpdatedInstance.