Migrar para as APIs Indication e Ripple

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

androidx.compose.foundation:foundation:1.7.0+ e androidx.compose.material:material-ripple:1.7.0+ incluem a seguinte API muda:

Descontinuado

Substituição

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

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

Observação: neste contexto, "Material library" (Bibliotecas do Material) refere-se a androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material e androidx.wear.compose:compose-material3.

RippleTheme

maneiras:

  • Use 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 de migração para as novas APIs.

Mudança de comportamento

As seguintes versões de 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 das bibliotecas do Material Design não usam mais rememberRipple(). Em vez disso, eles usam as novas APIs de ondulação. Como resultado, eles não consultam LocalRippleTheme. Portanto, se você definir LocalRippleTheme no seu aplicativo, o Material componentes não vão usar 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 Para mais instruções de migração, consulte Migrar do rememberRipple para o ripple. e nas próximas seções.

Fazer upgrade da versão da biblioteca Material sem migrar

Para desbloquear as versões atualizadas, use a API LocalUseFallbackRippleImplementation CompositionLocal para configurar Componentes do Material Design para voltar ao comportamento antigo:

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

Deixe isso fora do MaterialTheme para que as ondulações antigas possam ser fornecido 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 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()
    )
) {
    // ...
}

Você deve modificar 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 se lembrou. Ele também pode ser reutilizado em vários componentes, de modo semelhante por isso, considere extrair a criação de ondulação para um valor de nível superior para salvar alocações.

Como implementar um sistema de design personalizado

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

Migrar de 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 componentes do Material Design para voltar a usar rememberRipple. Assim, 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 de app personalizado criado com base no Material Design, poderá forneça com segurança o local de composição como parte do tema do app:

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

Para mais informações, consulte Fazer upgrade da versão da biblioteca Material sem seção sobre migração.

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 dos ondulações dentro de uma subárvore. Observe que RippleConfiguration e LocalRippleConfiguration são experimentais e destinadas apenas a componentes e personalização. A personalização global/em todo o tema não é compatível com estas APIs; consulte Como usar RippleTheme para alterar globalmente todas as ondulações em uma 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 {
            // ...
        }
    }

Você deve modificar o snippet acima para:

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

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

Como descrito na seção anterior, RippleConfiguration e As LocalRippleConfiguration são APIs experimentais e destinadas apenas a 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 {
            // ...
        }
    }

Você deve modificar 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 uma em todo o tema. Esse era basicamente um ponto de integração entre locais e ondulação da composição do sistema de design. Em vez de expor uma imagem primitivo de aplicação de temas, material-ripple agora expõe um createRippleModifierNode() função. Essa função permite que as bibliotecas do sistema de design criem que ordenam a implementação de wrapper, que consultam os valores do tema e delegam a implementação de ondulação no nó criado por esta função.

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

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

Migrar de Indication para IndicationNodeFactory

Percorrendo Indication

Se você estiver apenas criando um Indication para transmitir, como ao criar um ondulação para transmitir para Modifier.clickable ou Modifier.indication, você não precisará fazer alterações. IndicationNodeFactory herda de Indication, então tudo continuará a ser compilado e funcionando.

Criando Indication

Se você estiver criando sua própria implementação de Indication, a migração vai precisar ser simples na maioria dos casos. Por exemplo, considere um Indication que aplica uma 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: ele expõe um 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 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()
            }
        }
    }

    Você deve modificar 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 o a lógica de coleta é movida para o nó, esta é uma fábrica muito simples objeto, 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
        }
    }

    Você deve modificar 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, você precisa usar Modifier.indication para exibir Indication para um componente. No entanto, no caso raro de você criar manualmente um IndicationInstance usando rememberUpdatedInstance, você precisa atualizar seu implementação para verificar se o Indication é um IndicationNodeFactory. pode usar uma implementação mais leve. Por exemplo, Modifier.indication vai delegar internamente ao nó criado, se ele for um IndicationNodeFactory. Se não, ele vai usar Modifier.composed para chamar rememberUpdatedInstance.