Zu Indication und Ripple APIs migrieren

Zur Verbesserung der Kompositionsleistung interaktiver Komponenten, die Modifier.clickable verwenden, haben wir neue APIs eingeführt. Diese APIs ermöglichen effizientere Indication-Implementierungen, z. B. Ripples.

androidx.compose.foundation:foundation:1.7.0+ und androidx.compose.material:material-ripple:1.7.0+ enthalten die folgenden API-Änderungen:

Eingestellt

Ersatz

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Neue ripple()-APIs werden stattdessen in Material-Bibliotheken bereitgestellt.

Hinweis: In diesem Zusammenhang bezieht sich „Material-Bibliotheken“ auf androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material und androidx.wear.compose:compose-material3..

RippleTheme

Entweder:

  • Verwenden Sie die Material-Bibliothek RippleConfiguration APIs oder
  • Eigene Ripple-Implementierung für das Designsystem erstellen

Auf dieser Seite werden die Auswirkungen der Verhaltensänderung und die Anleitung für die Migration zu den neuen APIs beschrieben.

Verhaltensänderung

Die folgenden Bibliotheksversionen enthalten eine Änderung des Ripple-Verhaltens:

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

In diesen Versionen von Material-Bibliotheken wird rememberRipple() nicht mehr verwendet, sondern die neuen Ripple-APIs. Daher wird LocalRippleTheme nicht abgefragt. Wenn Sie LocalRippleTheme in Ihrer Anwendung festlegen, werden diese Werte nicht von Material-Komponenten verwendet.

In den folgenden Abschnitten wird beschrieben, wie Sie zu den neuen APIs migrieren.

Von rememberRipple zu ripple migrieren

Materialbibliothek verwenden

Wenn Sie eine Material-Bibliothek verwenden, ersetzen Sie rememberRipple() direkt durch einen Aufruf von ripple() aus der entsprechenden Bibliothek. Mit dieser API wird ein Ripple-Effekt mit Werten erstellt, die von den Material-Theme-APIs abgeleitet werden. Übergeben Sie das zurückgegebene Objekt dann an Modifier.clickable und/oder andere Komponenten.

Im folgenden Snippet werden beispielsweise die verworfenen APIs verwendet:

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

Sie sollten das obige Snippet so ändern:

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

ripple() ist keine zusammensetzbare Funktion mehr und muss nicht gespeichert werden. Sie kann auch für mehrere Komponenten wiederverwendet werden, ähnlich wie Modifikatoren. Sie sollten daher die Erstellung des Ripples in einen Wert auf oberster Ebene auslagern, um Zuweisungen zu sparen.

Benutzerdefiniertes Designsystem implementieren

Wenn Sie Ihr eigenes Designsystem implementieren und zuvor rememberRipple() zusammen mit einem benutzerdefinierten RippleTheme verwendet haben, um den Ripple-Effekt zu konfigurieren, sollten Sie stattdessen Ihre eigene Ripple-API bereitstellen, die an die in material-ripple bereitgestellten Ripple-Knoten-APIs delegiert. Ihre Komponenten können dann Ihre eigene Ripple-Implementierung verwenden, die Ihre Themenwerte direkt nutzt. Weitere Informationen finden Sie unter Von RippleTheme migrieren.

Von RippleTheme migrieren

RippleTheme verwenden, um einen Rippel-Effekt für eine bestimmte Komponente zu deaktivieren

Die Bibliotheken material und material3 stellen RippleConfiguration und LocalRippleConfiguration zur Verfügung, mit denen Sie das Erscheinungsbild von Ripples in einem Unterbaum konfigurieren können. RippleConfiguration und LocalRippleConfiguration sind experimentell und nur für die Anpassung einzelner Komponenten vorgesehen. Globale oder themenweite Anpassungen werden von diesen APIs nicht unterstützt. Weitere Informationen zu diesem Anwendungsfall finden Sie unter RippleTheme verwenden, um alle Ripples in einer Anwendung global zu ändern.

Im folgenden Snippet werden beispielsweise die verworfenen APIs verwendet:

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

Sie sollten das obige Snippet so ändern:

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

Mit RippleTheme die Farbe/den Alphawert einer Wellenanimation für eine bestimmte Komponente ändern

Wie im vorherigen Abschnitt beschrieben, sind RippleConfiguration und LocalRippleConfiguration experimentelle APIs, die nur für die Anpassung einzelner Komponenten vorgesehen sind.

Im folgenden Snippet werden beispielsweise die verworfenen APIs verwendet:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

Sie sollten das obige Snippet so ändern:

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

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

RippleTheme verwenden, um alle Ripples in einer Anwendung global zu ändern

Bisher konnten Sie LocalRippleTheme verwenden, um das Ripple-Verhalten auf Theme-Ebene zu definieren. Dies war im Grunde ein Integrationspunkt zwischen benutzerdefinierten lokalen Kompositionsvariablen für das Designsystem und Ripple. Anstelle eines generischen Theming-Primitivs wird in material-ripple jetzt eine createRippleModifierNode()-Funktion bereitgestellt. Diese Funktion ermöglicht es, in Designsystembibliotheken eine wrapper-Implementierung höherer Ordnung zu erstellen, die ihre Themenwerte abfragt und die Ripple-Implementierung dann an den von dieser Funktion erstellten Knoten delegiert.

So können Designsysteme direkt abfragen, was sie benötigen, und alle erforderlichen benutzerkonfigurierbaren Theming-Ebenen darüber bereitstellen, ohne sich an die Vorgaben der material-ripple-Ebene halten zu müssen. Durch diese Änderung wird auch deutlicher, welchem Theme bzw. welcher Spezifikation die Wellenanimation entspricht, da der Vertrag durch die Ripple API selbst definiert wird und nicht implizit vom Theme abgeleitet wird.

Eine Anleitung dazu finden Sie in der Implementierung der Ripple API in den Material-Bibliotheken. Ersetzen Sie die Aufrufe der lokalen Material-Komposition nach Bedarf für Ihr eigenes Designsystem.

Von Indication zu IndicationNodeFactory migrieren

Weitergabe von Indication

Wenn Sie nur ein Indication erstellen, um es weiterzugeben, z. B. um es an Modifier.clickable oder Modifier.indication zu übergeben, müssen Sie keine Änderungen vornehmen. IndicationNodeFactory erbt von Indication, daher wird alles weiterhin kompiliert und funktioniert.

Indication wird erstellt

Wenn Sie Ihre eigene Indication-Implementierung erstellen, sollte die Migration in den meisten Fällen einfach sein. Ein Beispiel für eine Indication, die einen Skalierungseffekt auf die Presse anwendet:

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

Die Migration kann in zwei Schritten erfolgen:

  1. Migrieren Sie ScaleIndicationInstance zu einem DrawModifierNode. Die API-Oberfläche für DrawModifierNode ähnelt sehr der von IndicationInstance: Sie stellt eine ContentDrawScope#draw()-Funktion bereit, die funktional mit IndicationInstance#drawContent() identisch ist. Sie müssen diese Funktion ändern und dann die collectLatest-Logik direkt im Knoten anstelle von Indication implementieren.

    Im folgenden Snippet werden beispielsweise die verworfenen APIs verwendet:

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

    Sie sollten das obige Snippet so ändern:

    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. Migrieren Sie ScaleIndication, um IndicationNodeFactory zu implementieren. Da die Erfassungslogik jetzt in den Knoten verschoben wird, ist dies ein sehr einfaches Fabrikobjekt, dessen einzige Aufgabe darin besteht, eine Knoteninstanz zu erstellen.

    Im folgenden Snippet werden beispielsweise die verworfenen APIs verwendet:

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

    Sie sollten das obige Snippet so ändern:

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

IndicationInstance mit Indication erstellen

In den meisten Fällen sollten Sie Modifier.indication verwenden, um Indication für eine Komponente anzuzeigen. Wenn Sie jedoch in dem seltenen Fall, dass Sie manuell ein IndicationInstance mit rememberUpdatedInstance erstellen, Ihre Implementierung aktualisieren müssen, um zu prüfen, ob das Indication ein IndicationNodeFactory ist, damit Sie eine einfachere Implementierung verwenden können. Wenn Modifier.indication beispielsweise ein IndicationNodeFactory ist, wird intern an den erstellten Knoten delegiert. Andernfalls wird Modifier.composed verwendet, um rememberUpdatedInstance aufzurufen.