Migracja do interfejsów Indication and Ripple API

Aby zwiększyć wydajność komponentów interaktywnych, które korzystają z Modifier.clickable, wprowadziliśmy nowe interfejsy API. Te interfejsy API umożliwiają bardziej wydajne implementacje Indication, takie jak efekty falowania.

androidx.compose.foundation:foundation:1.7.0+ iandroidx.compose.material:material-ripple:1.7.0+ obejmują te zmiany w API:

Wycofano

Wymiana

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Nowe interfejsy API ripple() są dostępne w bibliotekach Material.

Uwaga: w tym kontekście „biblioteki materiałów” odnoszą się do androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-materialandroidx.wear.compose:compose-material3..

RippleTheme

Wykonaj jedną z tych czynności:

  • używać interfejsów API biblioteki Material RippleConfiguration,
  • Tworzenie własnej implementacji efektu fali w systemie projektowania

Na tej stronie opisujemy wpływ zmiany działania i podajemy instrukcje migracji do nowych interfejsów API.

Zmiana zachowania

Zmiana działania efektu falowania została wprowadzona w tych wersjach biblioteki:

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

Te wersje bibliotek Material nie używają już rememberRipple(), tylko nowych interfejsów API efektu falowania. W efekcie nie wysyłają zapytania do LocalRippleTheme. Dlatego jeśli w aplikacji ustawisz LocalRippleTheme, komponenty Material nie będą używać tych wartości.

W sekcjach poniżej znajdziesz opis migracji do nowych interfejsów API.

Przenoszenie z rememberRipple do ripple

Korzystanie z biblioteki materiałów

Jeśli używasz biblioteki Material, zastąp bezpośrednio rememberRipple() wywołaniem ripple() z odpowiedniej biblioteki. Ten interfejs API tworzy efekt fali na podstawie wartości pochodzących z interfejsów API motywu Material. Następnie przekaż zwrócony obiekt do Modifier.clickable lub innych komponentów.

Na przykład ten fragment kodu używa wycofanych interfejsów API:

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

Zmodyfikuj powyższy fragment kodu w ten sposób:

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

Pamiętaj, że ripple() nie jest już funkcją kompozycyjną i nie trzeba jej zapamiętywać. Można go też używać w wielu komponentach, podobnie jak modyfikatorów, więc warto wyodrębnić tworzenie efektu fali do wartości najwyższego poziomu, aby zaoszczędzić alokacje.

Wdrażanie niestandardowego systemu projektowania

Jeśli wdrażasz własny system projektowania i wcześniej używałeś(-aś) rememberRipple() wraz z niestandardowym RippleTheme do konfigurowania efektu fali, zamiast tego podaj własny interfejs API efektu fali, który przekazuje wywołania do interfejsów API węzła efektu fali udostępnianych w material-ripple. Dzięki temu komponenty mogą używać własnego efektu fali, który bezpośrednio wykorzystuje wartości motywu. Więcej informacji znajdziesz w artykule Migracja z RippleTheme.

Przenoszenie z RippleTheme

Wyłączanie efektu fali dla danego komponentu za pomocą RippleTheme

Biblioteki materialmaterial3 udostępniają RippleConfigurationLocalRippleConfiguration, które pozwalają skonfigurować wygląd efektów falowania w poddrzewie. Pamiętaj, że RippleConfigurationLocalRippleConfiguration to funkcje eksperymentalne, które są przeznaczone wyłącznie do dostosowywania poszczególnych komponentów. Te interfejsy API nie obsługują dostosowywania globalnego ani w ramach motywu. Więcej informacji o tym przypadku użycia znajdziesz w artykule Używanie RippleTheme do globalnej zmiany wszystkich efektów falowania w aplikacji.

Na przykład ten fragment kodu używa wycofanych interfejsów API:

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

Zmodyfikuj powyższy fragment kodu w ten sposób:

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

Używanie RippleTheme do zmiany koloru lub wartości alfa efektu falowania w przypadku danego komponentu

Jak opisano w poprzedniej sekcji, interfejsy API RippleConfigurationLocalRippleConfiguration są eksperymentalne i służą wyłącznie do dostosowywania poszczególnych komponentów.

Na przykład ten fragment kodu używa wycofanych interfejsów API:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

Zmodyfikuj powyższy fragment kodu w ten sposób:

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

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

Używanie RippleTheme do globalnej zmiany wszystkich efektów falowania w aplikacji

Wcześniej można było używać LocalRippleTheme do określania zachowania efektu fali na poziomie całego motywu. Był to w zasadzie punkt integracji między lokalnymi kompozycjami niestandardowego systemu projektowania a biblioteką Ripple. Zamiast udostępniać ogólny element tematyczny, material-ripple udostępnia teraz funkcję createRippleModifierNode(). Ta funkcja umożliwia bibliotekom systemu projektowania tworzenie implementacji wrapper wyższego rzędu, które wysyłają zapytania o wartości motywu, a następnie przekazują implementację efektu fali do węzła utworzonego przez tę funkcję.

Umożliwia to systemom projektowania bezpośrednie wysyłanie zapytań o potrzebne informacje i udostępnianie wymaganych warstw motywów konfigurowanych przez użytkownika bez konieczności dostosowywania się do tego, co jest dostępne w warstwie material-ripple. Ta zmiana sprawia też, że bardziej wyraźnie widać, do jakiego motywu lub specyfikacji należy efekt fali, ponieważ to sam interfejs Ripple API definiuje ten kontrakt, a nie jest on niejawnie wywodzony z motywu.

Więcej informacji znajdziesz w implementacji interfejsu API ripple w bibliotekach Material. W razie potrzeby zastąp wywołania lokalnych funkcji kompozycji Material w swoim systemie projektowania.

Przenoszenie z Indication do IndicationNodeFactory

Przejazd około Indication

Jeśli tworzysz Indication, aby przekazać go dalej, np. tworzysz falę, którą chcesz przekazać Modifier.clickable lub Modifier.indication, nie musisz wprowadzać żadnych zmian. IndicationNodeFactory dziedziczy po Indication, więc wszystko będzie nadal kompilowane i działać.

Tworzę Indication

Jeśli tworzysz własną implementację Indication, w większości przypadków migracja powinna być prosta. Rozważmy na przykład Indication, który po naciśnięciu wywołuje efekt skalowania:

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

Możesz to zrobić w 2 krokach:

  1. Przenieś ScaleIndicationInstance, aby stał się DrawModifierNode. Interfejs API DrawModifierNode jest bardzo podobny do interfejsu IndicationInstance: udostępnia funkcję ContentDrawScope#draw(), która jest funkcjonalnie równoważna funkcji IndicationInstance#drawContent(). Musisz zmienić tę funkcję, a następnie zaimplementować logikę collectLatest bezpośrednio w węźle, a nie w Indication.

    Na przykład ten fragment kodu używa wycofanych interfejsów API:

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

    Zmodyfikuj powyższy fragment kodu w ten sposób:

    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. Przenieś ScaleIndication, aby wdrożyć IndicationNodeFactory. Logika kolekcji została przeniesiona do węzła, więc jest to bardzo prosty obiekt fabryczny, którego jedynym zadaniem jest utworzenie instancji węzła.

    Na przykład ten fragment kodu używa wycofanych interfejsów API:

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

    Zmodyfikuj powyższy fragment kodu w ten sposób:

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

Tworzenie IndicationInstance za pomocą Indication

W większości przypadków należy użyć Modifier.indication, aby wyświetlić Indication w przypadku komponentu. Jeśli jednak ręcznie tworzysz element IndicationInstance za pomocą funkcji rememberUpdatedInstance, musisz zaktualizować implementację, aby sprawdzić, czy element Indication jest elementem IndicationNodeFactory. Dzięki temu możesz użyć prostszej implementacji. Na przykład Modifier.indication wewnętrznie przekieruje żądanie do utworzonego węzła, jeśli jest on węzłem IndicationNodeFactory. W przeciwnym razie użyje Modifier.composed, aby zadzwonić pod numer rememberUpdatedInstance.