Zu Indication und Ripple APIs migrieren

Um die Kompositionsleistung interaktiver Komponenten zu verbessern, die Modifier.clickable verwenden, haben wir neue APIs eingeführt. Diese APIs ermöglichen effizientere Indication-Implementierungen, z. B. Welleneffekte.

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

Eingestellt

Ersatz

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Stattdessen werden neue ripple()-APIs in Material-Bibliotheken bereitgestellt.

Hinweis: In diesem Kontext 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 RippleConfiguration-APIs der Material-Bibliothek oder
  • Eigene Implementierung des Welleneffekts für Ihr 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 der Material-Bibliotheken wird rememberRipple() nicht mehr verwendet. Stattdessen werden die neuen Ripple-APIs verwendet. Daher wird LocalRippleTheme nicht abgefragt. Wenn Sie also LocalRippleTheme in Ihrer Anwendung festlegen, werden diese Werte von Material-Komponenten nicht 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. Diese API erstellt einen Ripple-Effekt mit Werten, die von den Material Theme APIs abgeleitet werden. Übergeben Sie dann das zurückgegebene Objekt 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()
    )
) {
    // ...
}

Ändern Sie das obige Snippet so:

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

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

Benutzerdefiniertes Designsystem wird implementiert

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. Anschließend können Ihre Komponenten Ihren eigenen Ripple-Effekt verwenden, der Ihre Designwerte direkt nutzt. Weitere Informationen finden Sie unter Von RippleTheme migrieren.

Von RippleTheme migrieren

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

Die Bibliotheken material und material3 stellen RippleConfiguration und LocalRippleConfiguration bereit, mit denen Sie das Erscheinungsbild von Welleneffekten in einem Unterbaum konfigurieren können. RippleConfiguration und LocalRippleConfiguration sind nur für die Anpassung pro Komponente vorgesehen. Globale/themenweite Anpassungen werden mit diesen APIs nicht unterstützt. Weitere Informationen zu diesem Anwendungsfall finden Sie unter RippleTheme verwenden, um alle Welleneffekte 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 {
            // ...
        }
    }

Ändern Sie das obige Snippet so:

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

Mit RippleTheme die Farbe/den Alphawert eines Welleneffekts für eine bestimmte Komponente ändern

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

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

Ändern Sie das obige Snippet so:

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

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

Mit RippleTheme alle Ripple-Effekte in einer Anwendung global ändern

Bisher konntest du mit LocalRippleTheme das Ripple-Verhalten auf Designebene definieren. Dies war im Wesentlichen ein Integrationspunkt zwischen benutzerdefinierten Designsystem-Kompositionslokalen und Ripple. Anstatt ein generisches Design-Primitive bereitzustellen, stellt material-ripple jetzt eine createRippleModifierNode()-Funktion bereit. Mit dieser Funktion können Designsystembibliotheken eine wrapper-Implementierung höherer Ordnung erstellen, die ihre Designwerte abfragt und dann die Ripple-Implementierung an den von dieser Funktion erstellten Knoten delegiert.

So können Designsysteme direkt abfragen, was sie benötigen, und alle erforderlichen, vom Nutzer konfigurierbaren Designebenen darüber bereitstellen, ohne sich an die Vorgaben der material-ripple-Ebene halten zu müssen. Durch diese Änderung wird auch deutlicher, welchem Design bzw. welcher Spezifikation der Ripple entspricht, da die Ripple API selbst diesen Vertrag definiert, anstatt ihn implizit aus dem Design abzuleiten.

Eine Anleitung dazu findest du in der Ripple API-Implementierung in Material-Bibliotheken. Ersetze die Aufrufe von Material-CompositionLocals nach Bedarf für dein eigenes Designsystem.

Von Indication zu IndicationNodeFactory migrieren

Vorbeifahrt gegen Indication

Wenn Sie nur ein Indication erstellen, um es weiterzugeben, z. B. einen Ripple-Effekt, der an Modifier.clickable oder Modifier.indication übergeben werden soll, müssen Sie keine Änderungen vornehmen. IndicationNodeFactory übernimmt von Indication, sodass alles weiterhin kompiliert wird 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 beim Drücken einen Skalierungseffekt 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 die Migration in zwei Schritten durchführen:

  1. Migrieren Sie ScaleIndicationInstance zu einem DrawModifierNode. Die API-Oberfläche für DrawModifierNode ist IndicationInstance sehr ähnlich: 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 implementieren, anstatt die Indication zu verwenden.

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

    Ändern Sie das obige Snippet so:

    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 wurde, ist dies ein sehr einfaches Factory-Objekt, 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
        }
    }

    Ändern Sie das obige Snippet so:

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

Indication zum Erstellen eines IndicationInstance verwenden

In den meisten Fällen sollten Sie Modifier.indication verwenden, um Indication für eine Komponente anzuzeigen. In dem seltenen Fall, dass du manuell ein IndicationInstance mit rememberUpdatedInstance erstellst, musst du deine Implementierung aktualisieren, um zu prüfen, ob das Indication ein IndicationNodeFactory ist, damit du eine einfachere Implementierung verwenden kannst. Beispiel: Modifier.indication wird intern an den erstellten Knoten delegiert, wenn es sich um einen IndicationNodeFactory handelt. Andernfalls wird Modifier.composed verwendet, um rememberUpdatedInstance aufzurufen.