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 Indicationimplementacje, np. efekt falowania.

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

Wycofano

Urządzenie zamienne

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

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

Uwaga: w tym kontekście „biblioteki materiałów” oznaczają androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material i androidx.wear.compose:compose-material3..

RippleTheme

Wykonaj jedną z tych czynności:

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

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

Zmiana zachowania

Zmiana w działaniu efektu fali 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(), ale nowych interfejsów API efektu falowania. W związku z tym nie wysyłają zapytania 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, bezpośrednio zastąp rememberRipple() wywołaniem ripple() z odpowiedniej biblioteki. Ten interfejs API tworzy efekt fali za pomocą wartości pochodzących z interfejsów API motywu Material. Następnie przekaż zwrócony obiekt do funkcji 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ą złożoną i nie musisz o niej pamiętać. Można go też ponownie wykorzystać w wielu komponentach, podobnie jak modyfikatory, więc warto wyodrębnić tworzenie efektu fali do wartości najwyższego poziomu, aby zaoszczędzić przydziały.

Wdrażanie niestandardowego systemu projektowania

Jeśli wdrażasz własny system projektowania i wcześniej używałeś elementu rememberRipple() wraz z niestandardowym elementem 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 zRippleTheme.

Przenoszenie z RippleTheme

Wyłączanie efektu fali w przypadku danego komponentu za pomocą atrybutu RippleTheme

Biblioteki material i material3 udostępniają RippleConfiguration i LocalRippleConfiguration, które umożliwiają skonfigurowanie wyglądu efektów falowania w poddrzewie. Pamiętaj, że parametry RippleConfiguration i LocalRippleConfiguration są przeznaczone tylko 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 przezroczystości efektu fali dla danego komponentu

Jak opisano w poprzedniej sekcji, symbole RippleConfiguration i LocalRippleConfiguration są przeznaczone tylko 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 definiowania 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 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 falowania do węzła utworzonego przez tę funkcję.

Pozwala to systemom projektowania bezpośrednio wysyłać zapytania o potrzebne informacje i udostępniać wszelkie wymagane warstwy motywów konfigurowane 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 efektu falowania w bibliotekach Material. W razie potrzeby zastąp wywołania lokalnych kompozycji Material w swoim systemie projektowania.

Przenoszenie z Indication do IndicationNodeFactory

Przejazd około Indication

Jeśli tworzysz tylko Indication do przekazania dalej, np. tworzysz falę do przekazania do Modifier.clickable lub Modifier.indication, nie musisz wprowadzać żadnych zmian. IndicationNodeFactory dziedziczy po Indication, więc wszystko będzie nadal się kompilować i działać.

Tworzę Indication

Jeśli tworzysz własną implementację Indication, migracja w większości przypadków powinna być prosta. Weźmy na przykład Indication, który po naciśnięciu powoduje 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. Powierzchnia interfejsu API DrawModifierNode jest bardzo podobna do IndicationInstance: udostępnia funkcję ContentDrawScope#draw(), która jest funkcjonalnie równoważna z 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żywać Modifier.indication, aby wyświetlić Indication dla 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żywać prostszej implementacji. Na przykład Modifier.indication wewnętrznie przekaże zadanie utworzonemu węzłowi, jeśli jest on węzłem IndicationNodeFactory. W przeciwnym razie użyje Modifier.composed do wywołania rememberUpdatedInstance.