Zu Indication und Ripple APIs migrieren

Zur Verbesserung der Zusammensetzungsleistung von interaktiven Komponenten, die Modifier.clickable verwenden, haben wir neue APIs eingeführt. Diese APIs ermöglichen effizientere Indication-Implementierungen wie 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()

Stattdessen werden neue ripple() APIs in Materialbibliotheken bereitgestellt.

Hinweis: In diesem Kontext bezieht sich „Materialbibliotheken“ auf androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material und androidx.wear.compose:compose-material3..

RippleTheme

Sie haben zwei Möglichkeiten:

  • die RippleConfiguration APIs der Material Library verwenden oder
  • Ripple-Implementierung für Ihr eigenes Designsystem erstellen

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

Verhaltensänderung

Die folgenden Bibliotheksversionen beinhalten 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+

Diese Versionen der Materialbibliotheken verwenden nicht mehr rememberRipple(), sondern die neuen Ripple APIs. Daher wird LocalRippleTheme nicht abgefragt. Wenn Sie in Ihrer Anwendung LocalRippleTheme festlegen, werden diese Werte daher nicht von Materialkomponenten verwendet.

Im folgenden Abschnitt wird beschrieben, wie Sie vorübergehend zum alten Verhalten zurückkehren, ohne zu migrieren. Wir empfehlen jedoch, zu den neuen APIs zu migrieren. Eine Migrationsanleitung finden Sie unter Von rememberRipple zu ripple migrieren und in den nachfolgenden Abschnitten.

Version der Material Library ohne Migration aktualisieren

Wenn Sie die Blockierung der Aktualisierung von Bibliotheksversionen aufheben möchten, können Sie mit der temporären LocalUseFallbackRippleImplementation CompositionLocal API Materialkomponenten so konfigurieren, dass sie auf das alte Verhalten zurückgesetzt werden:

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

Diese muss außerhalb von MaterialTheme angegeben werden, damit die alten Wellen über LocalIndication bereitgestellt werden können.

In den folgenden Abschnitten wird die Migration zu den neuen APIs beschrieben.

Von rememberRipple nach ripple migrieren

Materialbibliothek verwenden

Wenn Sie eine Material-Bibliothek verwenden, ersetzen Sie rememberRipple() direkt durch einen Aufruf von ripple() aus der entsprechenden Bibliothek. Diese API erzeugt eine Welle mit Werten, die aus den Material The APIs abgeleitet sind. Übergeben Sie dann das zurückgegebene Objekt an Modifier.clickable und/oder andere Komponenten.

Im folgenden Snippet werden beispielsweise die eingestellten APIs verwendet:

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

Ändern Sie das obige Snippet wie folgt:

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

ripple() ist keine zusammensetzbare Funktion mehr und muss nicht mehr berücksichtigt werden. Sie kann ähnlich wie Modifizierer auch für mehrere Komponenten wiederverwendet werden. Sie sollten daher die Erstellung der Wellenform auf einen Wert auf oberster Ebene extrahieren, um Zuweisungen zu speichern.

Implementierung eines benutzerdefinierten Designsystems

Wenn Sie ein eigenes Designsystem implementieren und bisher rememberRipple() zusammen mit einem benutzerdefinierten RippleTheme zum Konfigurieren der Wellen verwendet haben, sollten Sie stattdessen Ihre eigene Ripple API bereitstellen, die an die in material-ripple bereitgestellten Ripple Node APIs delegiert. Anschließend können Ihre Komponenten eine eigene Welle verwenden, die Ihre Themenwerte direkt aufnimmt. Weitere Informationen finden Sie unter Von RippleTheme migrieren.

Von RippleTheme migrieren

Verhaltensänderungen vorübergehend deaktivieren

Materialbibliotheken haben eine temporäre CompositionLocal, LocalUseFallbackRippleImplementation, mit der Sie alle Materialkomponenten so konfigurieren können, dass sie rememberRipple verwenden. Auf diese Weise fragt rememberRipple weiterhin LocalRippleTheme ab.

Das folgende Code-Snippet zeigt, wie die LocalUseFallbackRippleImplementation CompositionLocal API verwendet wird:

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

Wenn du ein benutzerdefiniertes App-Design verwendest, das auf Material basiert, kannst du die lokale Komposition sicher als Teil des App-Designs bereitstellen:

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

Weitere Informationen finden Sie im Abschnitt Version der Material Library ohne Migration upgraden.

Mit RippleTheme die Wellen bei einer bestimmten Komponente deaktivieren

Die Bibliotheken material und material3 stellen RippleConfiguration und LocalRippleConfiguration zur Verfügung. Damit können Sie die Darstellung von Wellen innerhalb einer Unterstruktur konfigurieren. RippleConfiguration und LocalRippleConfiguration sind experimentell und nur für die Anpassung pro Komponente vorgesehen. Die globale bzw. themeweite Anpassung wird für diese APIs nicht unterstützt. Weitere Informationen zu diesem Anwendungsfall finden Sie unter Mit RippleTheme alle Wellen in einer Anwendung global ändern.

Im folgenden Snippet werden beispielsweise die eingestellten 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 {
            // ...
        }
    }

Ändern Sie das obige Snippet wie folgt:

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

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

Mit RippleTheme die Farbe/Alpha der Welle für eine bestimmte Komponente ändern

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

Im folgenden Snippet werden beispielsweise die eingestellten APIs verwendet:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

Ändern Sie das obige Snippet wie folgt:

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

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

Mit RippleTheme alle Wellen in einer App global ändern

Bisher konnten Sie mit LocalRippleTheme das Verhalten der Wellenlinie auf Theme-Ebene definieren. Dies war im Wesentlichen ein Integrationspunkt zwischen benutzerdefinierten Systemzusammensetzungen und Ripple. Anstatt eine generische Themen-Primitive offenzulegen, stellt material-ripple jetzt eine createRippleModifierNode()-Funktion zur Verfügung. Mit dieser Funktion können Designsystembibliotheken höhere wrapper-Implementierungen erstellen, die ihre Themenwerte abfragen und dann die Ripple-Implementierung an den von dieser Funktion erstellten Knoten delegieren.

Auf diese Weise können Designsysteme direkt abfragen, was sie brauchen, und die erforderlichen, vom Nutzer konfigurierbaren Themenebenen darüber bereitstellen, ohne die Anforderungen der material-ripple-Ebene erfüllen zu müssen. Durch diese Änderung wird auch deutlicher, welchem Thema bzw. welcher Spezifikation die Ripple entspricht, da diese von der Ripple API selbst definiert wird, anstatt implizit vom Thema abgeleitet zu werden.

Eine Anleitung finden Sie unter Ripple API-Implementierung in Materialbibliotheken. Ersetzen Sie die Aufrufe von lokalen Materialkompositionen, die für Ihr eigenes Designsystem erforderlich sind.

Von Indication nach IndicationNodeFactory migrieren

Du passierst ca. Indication

Wenn Sie nur ein Indication zur Weitergabe erstellen, z. B. eine Welle zur Übergabe an Modifier.clickable oder Modifier.indication, müssen Sie keine Änderungen vornehmen. IndicationNodeFactory übernimmt von Indication, sodass alles weiter kompiliert und funktioniert.

Indication wird erstellt

Wenn Sie eine eigene Indication-Implementierung erstellen, sollte die Migration in den meisten Fällen einfach sein. Betrachten Sie beispielsweise ein Indication, das einen Skaleneffekt auf das Drücken 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()
        }
    }
}

Sie können dies in zwei Schritten migrieren:

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

    Im folgenden Snippet werden beispielsweise die eingestellten 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()
            }
        }
    }

    Ändern Sie das obige Snippet wie folgt:

    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 Factory-Objekt, dessen einzige Aufgabe darin besteht, eine Knoteninstanz zu erstellen.

    Im folgenden Snippet werden beispielsweise die eingestellten 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
        }
    }

    Ändern Sie das obige Snippet wie folgt:

    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. In dem seltenen Fall, dass Sie manuell ein IndicationInstance mit rememberUpdatedInstance erstellen, müssen Sie Ihre Implementierung aktualisieren, um zu prüfen, ob Indication ein IndicationNodeFactory ist. Dann können Sie eine einfachere Implementierung verwenden. Modifier.indication delegiert beispielsweise intern an den erstellten Knoten, wenn er ein IndicationNodeFactory ist. Andernfalls wird Modifier.composed verwendet, um rememberUpdatedInstance aufzurufen.