Esegui la migrazione alle API Indication e Ripple

Per migliorare le prestazioni della composizione dei componenti interattivi che utilizzano Modifier.clickable, abbiamo introdotto nuove API. Queste API consentono implementazioni Indication più efficienti, come gli echi.

androidx.compose.foundation:foundation:1.7.0+ e androidx.compose.material:material-ripple:1.7.0+ includono le seguenti modifiche all'API:

Obsoleta

Sostituzione

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Nuove API ripple() fornite nelle librerie Material.

Nota: in questo contesto, per "librerie materiali" si intendono androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material e androidx.wear.compose:compose-material3.

RippleTheme

Procedi in uno dei seguenti modi:

  • Usa le API Material Library RippleConfiguration oppure
  • Crea l'implementazione Ripple del tuo sistema di progettazione

Questa pagina descrive l'impatto delle modifiche del comportamento e le istruzioni per eseguire la migrazione alle nuove API.

Modifica del comportamento

Le seguenti versioni delle librerie includono una modifica del comportamento dell'ondulazione:

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

Queste versioni delle librerie Material non utilizzano più rememberRipple(), ma le nuove API Ripple. Di conseguenza, non eseguono query su LocalRippleTheme. Di conseguenza, se imposti LocalRippleTheme nell'applicazione, i componenti Material non utilizzeranno questi valori.

La sezione seguente descrive come ripristinare temporaneamente il comportamento precedente senza eseguire la migrazione; tuttavia, ti consigliamo di eseguire la migrazione alle nuove API. Per istruzioni sulla migrazione, consulta Eseguire la migrazione da rememberRipple a ripple e le sezioni successive.

Esegui l'upgrade della versione della libreria di materiali senza eseguire la migrazione

Per sbloccare l'upgrade delle versioni della libreria, puoi utilizzare l'API LocalUseFallbackRippleImplementation CompositionLocal temporanea per configurare i componenti Material in modo che tornino al comportamento precedente:

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

Assicurati di specificare questo valore all'esterno di MaterialTheme, in modo che le ecosce precedenti possano essere fornite tramite LocalIndication.

Le sezioni seguenti descrivono come eseguire la migrazione alle nuove API.

Esegui la migrazione da rememberRipple a ripple

Utilizzo di una libreria di materiali

Se utilizzi una libreria di Material, sostituisci direttamente rememberRipple() con una chiamata a ripple() dalla libreria corrispondente. Questa API crea un'onda utilizzando i valori derivati dalle API Material Subject. Quindi, passa l'oggetto restituito a Modifier.clickable e/o ad altri componenti.

Ad esempio, il seguente snippet utilizza le API ritirate:

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

Devi modificare lo snippet riportato sopra in:

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

Tieni presente che ripple() non è più una funzione componibile e non deve essere memorizzato. Può anche essere riutilizzato in più componenti, come accade per i modificatori, quindi ti consigliamo di estrarre la creazione dell'onda in un valore di primo livello per risparmiare le allocazioni.

Implementazione di un sistema di progettazione personalizzato

Se stai implementando il tuo sistema di progettazione, che in precedenza utilizzavi rememberRipple() insieme a un RippleTheme personalizzato per configurare l'onda, dovresti fornire la tua API Ripple che delega alle API del nodo Ripple esposte in material-ripple. Quindi, i componenti possono utilizzare la tua ondulazione che consuma direttamente i valori del tema. Per maggiori informazioni, consulta Eseguire la migrazione daRippleTheme.

Esegui la migrazione da RippleTheme

Disattivare temporaneamente la modifica del comportamento

Le librerie di Material hanno un valore CompositionLocal temporaneo, LocalUseFallbackRippleImplementation, che puoi utilizzare per configurare tutti i componenti Material in modo che utilizzino rememberRipple. In questo modo, rememberRipple continua a eseguire query su LocalRippleTheme.

Il seguente snippet di codice mostra come utilizzare l'API LocalUseFallbackRippleImplementation CompositionLocal:

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

Se utilizzi un tema dell'app personalizzato basato su Material, puoi fornire in sicurezza la composizione locale come parte del tema dell'app:

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

Per ulteriori informazioni, consulta la sezione Eseguire l'upgrade della versione della raccolta di materiali senza migrazione.

Utilizzo di RippleTheme per disattivare un'eco per un determinato componente

Le librerie material e material3 espongono RippleConfiguration e LocalRippleConfiguration, permettendoti di configurare l'aspetto delle ecosce all'interno di un sottoalbero. Tieni presente che RippleConfiguration e LocalRippleConfiguration sono sperimentali e sono destinati solo alla personalizzazione di ciascun componente. La personalizzazione globale/a livello di tema non è supportata con queste API. Per ulteriori informazioni sul caso d'uso specifico, consulta Utilizzare RippleTheme per modificare a livello globale tutti gli echi in un' applicazione.

Ad esempio, il seguente snippet utilizza le API ritirate:

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 {
            // ...
        }
    }

Devi modificare lo snippet riportato sopra in:

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

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

Utilizzo di RippleTheme per modificare il colore/alfa di un'onda per un determinato componente

Come descritto nella sezione precedente, RippleConfiguration e LocalRippleConfiguration sono API sperimentali e sono destinate solo alla personalizzazione per componente.

Ad esempio, il seguente snippet utilizza le API ritirate:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

Devi modificare lo snippet riportato sopra in:

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

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

Utilizzo di RippleTheme per modificare a livello globale tutte le echi in un'applicazione

In precedenza, potevi utilizzare LocalRippleTheme per definire il comportamento dell'onda a livello di tema. Si tratta essenzialmente di un punto di integrazione tra locali di composizione di sistemi di progettazione personalizzati e ripple. Anziché esporre una primitiva di tema generico, material-ripple ora espone una funzione createRippleModifierNode(). Questa funzione consente alle librerie di sistemi di progettazione di creare un'implementazione wrapper di ordine superiore, che eseguono query sui valori dei temi e poi delegano l'implementazione dell'onda al nodo creato da questa funzione.

Ciò consente ai sistemi di progettazione di eseguire query direttamente su ciò di cui hanno bisogno e di esporre in alto tutti i livelli di tema configurabili dall'utente necessari senza doversi conformarsi a ciò che viene fornito al livello material-ripple. Questa modifica rende anche più esplicito il tema/la specifica a cui si conforma il ripple, in quanto è l'API stessa a definire il contratto, piuttosto che essere implicitamente derivata dal tema.

Per indicazioni, consulta l'implementazione dell'API Ripple nelle librerie di Material e sostituisci le chiamate agli utenti locali della composizione di Material in base alle esigenze del tuo sistema di progettazione.

Esegui la migrazione da Indication a IndicationNodeFactory

Passaggi nelle vicinanze di Indication

Se stai solo creando un Indication da passare, ad esempio creando un eco da passare a Modifier.clickable o Modifier.indication, non devi apportare alcuna modifica. IndicationNodeFactory eredita da Indication, quindi tutto continuerà a essere compilato e a funzionare.

Creazione di Indication in corso...

Se stai creando la tua implementazione Indication, la migrazione dovrebbe essere semplice nella maggior parte dei casi. Ad esempio, considera un Indication che applica un effetto di scala sulla stampa:

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()
        }
    }
}

Puoi eseguire la migrazione in due passaggi:

  1. Esegui la migrazione di ScaleIndicationInstance in un DrawModifierNode. La piattaforma API per DrawModifierNode è molto simile a IndicationInstance: espone una funzione ContentDrawScope#draw() che è funzionalmente equivalente a IndicationInstance#drawContent(). Devi modificare questa funzione e quindi implementare la logica collectLatest direttamente all'interno del nodo, anziché Indication.

    Ad esempio, il seguente snippet utilizza le API ritirate:

    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()
            }
        }
    }

    Devi modificare lo snippet riportato sopra in:

    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. Esegui la migrazione di ScaleIndication per implementare IndicationNodeFactory. Poiché la logica di raccolta ora viene spostata nel nodo, si tratta di un oggetto di fabbrica molto semplice la cui unica responsabilità è creare un'istanza del nodo.

    Ad esempio, il seguente snippet utilizza le API ritirate:

    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
        }
    }

    Devi modificare lo snippet riportato sopra in:

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

Utilizzo di Indication per creare un IndicationInstance

Nella maggior parte dei casi, devi utilizzare Modifier.indication per visualizzare Indication di un componente. Tuttavia, nel raro caso in cui tu stia creando manualmente un IndicationInstance utilizzando rememberUpdatedInstance, devi aggiornare l'implementazione per verificare se Indication è un IndicationNodeFactory, in modo da poter utilizzare un'implementazione più leggera. Ad esempio, Modifier.indication delegherà internamente al nodo creato se si tratta di un IndicationNodeFactory. In caso contrario, verrà utilizzato Modifier.composed per chiamare rememberUpdatedInstance.