Migrer vers les API Indication et Ripple

Pour améliorer les performances de composition des composants interactifs qui utilisent Modifier.clickable, nous avons introduit de nouvelles API. Ces API permettent des implémentations Indication plus efficaces, telles que les ondulations.

androidx.compose.foundation:foundation:1.7.0+ et androidx.compose.material:material-ripple:1.7.0+ incluent les modifications d'API suivantes :

Obsolète

Remplacement

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Nouvelles API ripple() fournies dans les bibliothèques Material à la place.

Remarque : Dans ce contexte, "bibliothèques Material" fait référence à androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material et androidx.wear.compose:compose-material3..

RippleTheme

L'une des options ci-dessous :

  • Utilisez les API RippleConfiguration de la bibliothèque Material.
  • Créer votre propre implémentation de l'effet d'ondulation dans un système de conception

Cette page décrit l'impact du changement de comportement et fournit des instructions pour migrer vers les nouvelles API.

Nouveau comportement

Les versions de bibliothèque suivantes incluent un changement de comportement de l'effet d'ondulation :

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

Ces versions des bibliothèques Material n'utilisent plus rememberRipple(), mais les nouvelles API d'ondulation. Par conséquent, ils n'interrogent pas LocalRippleTheme. Par conséquent, si vous définissez LocalRippleTheme dans votre application, les composants Material n'utiliseront pas ces valeurs.

Les sections suivantes expliquent comment migrer vers les nouvelles API.

Migrer de rememberRipple vers ripple

Utiliser une bibliothèque Material

Si vous utilisez une bibliothèque Material, remplacez directement rememberRipple() par un appel à ripple() à partir de la bibliothèque correspondante. Cette API crée une ondulation à l'aide de valeurs dérivées des API de thématisation Material. Transmettez ensuite l'objet renvoyé à Modifier.clickable et/ou à d'autres composants.

Par exemple, l'extrait de code suivant utilise les API obsolètes :

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

Vous devez modifier l'extrait de code ci-dessus comme suit :

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

Notez que ripple() n'est plus une fonction composable et qu'il n'est pas nécessaire de la mémoriser. Il peut également être réutilisé dans plusieurs composants, comme les modificateurs. Envisagez donc d'extraire la création de l'effet d'ondulation vers une valeur de niveau supérieur pour économiser des allocations.

Implémentation d'un système de conception personnalisé

Si vous implémentez votre propre système de conception et que vous utilisiez auparavant rememberRipple() avec un RippleTheme personnalisé pour configurer l'effet d'ondulation, vous devez fournir votre propre API d'effet d'ondulation qui délègue aux API de nœud d'effet d'ondulation exposées dans material-ripple. Vos composants peuvent ensuite utiliser votre propre ondulation qui consomme directement les valeurs de votre thème. Pour en savoir plus, consultez Migrer depuis RippleTheme.

Migrer depuis RippleTheme

Utiliser RippleTheme pour désactiver un effet d'ondulation pour un composant donné

Les bibliothèques material et material3 exposent RippleConfiguration et LocalRippleConfiguration, qui vous permettent de configurer l'apparence des ondulations dans une sous-arborescence. Notez que RippleConfiguration et LocalRippleConfiguration ne sont destinés qu'à la personnalisation par composant. La personnalisation globale/à l'échelle du thème n'est pas compatible avec ces API. Pour en savoir plus sur ce cas d'utilisation, consultez Utiliser RippleTheme pour modifier globalement tous les effets d'ondulation d'une application.

Par exemple, l'extrait de code suivant utilise les API obsolètes :

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

Vous devez modifier l'extrait de code ci-dessus comme suit :

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

Utiliser RippleTheme pour modifier la couleur/l'alpha d'un effet d'ondulation pour un composant donné

Comme décrit dans la section précédente, RippleConfiguration et LocalRippleConfiguration ne sont destinés qu'à la personnalisation par composant.

Par exemple, l'extrait de code suivant utilise les API obsolètes :

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

Vous devez modifier l'extrait de code ci-dessus comme suit :

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

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

Utiliser RippleTheme pour modifier globalement toutes les ondulations d'une application

Auparavant, vous pouviez utiliser LocalRippleTheme pour définir le comportement de l'effet d'ondulation au niveau du thème. Il s'agissait essentiellement d'un point d'intégration entre les CompositionLocals du système de conception personnalisé et l'effet d'ondulation. Au lieu d'exposer une primitive de thématisation générique, material-ripple expose désormais une fonction createRippleModifierNode(). Cette fonction permet aux bibliothèques de systèmes de conception de créer une implémentation wrapper d'ordre supérieur, qui interroge les valeurs de leur thème, puis délègue l'implémentation de l'effet d'ondulation au nœud créé par cette fonction.

Cela permet aux systèmes de conception d'interroger directement ce dont ils ont besoin et d'exposer toutes les couches de thèmes configurables par l'utilisateur requises, sans avoir à se conformer à ce qui est fourni au niveau de la couche material-ripple. Ce changement permet également de définir plus explicitement le thème/la spécification auquel l'effet d'ondulation est conforme, car c'est l'API d'ondulation elle-même qui définit ce contrat, plutôt que d'être implicitement dérivée du thème.

Pour obtenir des conseils, consultez l'implémentation de l'API Ripple dans les bibliothèques Material et remplacez les appels aux CompositionLocals Material si nécessaire pour votre propre système de conception.

Migrer de Indication vers IndicationNodeFactory

Passage vers Indication

Si vous créez simplement un Indication à transmettre, par exemple un effet d'ondulation à transmettre à Modifier.clickable ou Modifier.indication, vous n'avez pas besoin d'apporter de modifications. IndicationNodeFactory hérite de Indication. Tout continuera donc à se compiler et à fonctionner.

Création de Indication

Si vous créez votre propre implémentation Indication, la migration devrait être simple dans la plupart des cas. Par exemple, prenons un Indication qui applique un effet de mise à l'échelle lors d'un appui :

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

Vous pouvez effectuer cette migration en deux étapes :

  1. Migrez ScaleIndicationInstance pour en faire un DrawModifierNode. La surface de l'API pour DrawModifierNode est très semblable à celle de IndicationInstance : elle expose une fonction ContentDrawScope#draw() qui est fonctionnellement équivalente à IndicationInstance#drawContent(). Vous devez modifier cette fonction, puis implémenter la logique collectLatest directement dans le nœud, au lieu de Indication.

    Par exemple, l'extrait de code suivant utilise les API obsolètes :

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

    Vous devez modifier l'extrait de code ci-dessus comme suit :

    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. Migrez ScaleIndication pour implémenter IndicationNodeFactory. La logique de collecte ayant été déplacée dans le nœud, il s'agit d'un objet d'usine très simple dont la seule responsabilité est de créer une instance de nœud.

    Par exemple, l'extrait de code suivant utilise les API obsolètes :

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

    Vous devez modifier l'extrait de code ci-dessus comme suit :

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

Créer un IndicationInstance à l'aide de Indication

Dans la plupart des cas, vous devez utiliser Modifier.indication pour afficher Indication pour un composant. Toutefois, dans les rares cas où vous créez manuellement un IndicationInstance à l'aide de rememberUpdatedInstance, vous devez mettre à jour votre implémentation pour vérifier si Indication est un IndicationNodeFactory afin de pouvoir utiliser une implémentation plus légère. Par exemple, Modifier.indication délèguera en interne au nœud créé s'il s'agit d'un IndicationNodeFactory. Sinon, il utilisera Modifier.composed pour appeler rememberUpdatedInstance.