Migracja do interfejsów Indication and Ripple API

Aby poprawić wydajność kompozycji interaktywnych komponentów, które używają Modifier.clickable, wprowadziliśmy nowe interfejsy API. Te interfejsy API umożliwiają bardziej wydajne implementacje Indication, takie jak echo.

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

Wycofano

Zamiennik

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Zamiast nich udostępniliśmy nowe interfejsy API ripple() w bibliotekach Material.

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

RippleTheme

Możesz:

  • użyć interfejsów API RippleConfiguration biblioteki Material lub
  • Utwórz własną implementację echa systemu projektowania

Na tej stronie opisujemy wpływ zmian w działaniu oraz instrukcje migracji do nowych interfejsów API.

Zmiana działania

Te wersje biblioteki zawierają zmianę działania:

  • 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(); zamiast tego korzystają z nowych interfejsów API Ripple. W związku z tym nie wysyłają zapytań o LocalRippleTheme. Dlatego jeśli ustawisz w aplikacji właściwość LocalRippleTheme, Komponenty Material Design nie będą używać tych wartości.

W tej sekcji dowiesz się, jak tymczasowo wrócić do starego sposobu działania bez migracji, ale zalecamy przejście na nowe interfejsy API. Instrukcje migracji znajdziesz w artykule Migracja z rememberRipple do ripple i kolejnych sekcjach.

Uaktualnij wersję biblioteki Material bez migracji

Aby odblokować uaktualnianie wersji biblioteki, możesz za pomocą tymczasowego interfejsu API LocalUseFallbackRippleImplementation CompositionLocal skonfigurować komponenty Material Design, aby wrócić do starego działania:

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

Pamiętaj, aby udostępnić go poza MaterialTheme, tak aby stare echa mogły być dostarczane przez LocalIndication.

W sekcjach poniżej dowiesz się, jak przejść na nowe interfejsy API.

Migracja z rememberRipple do ripple

Korzystanie z biblioteki Material

Jeśli używasz biblioteki Material, zastąp rememberRipple() bezpośrednio wywołaniem ripple() z odpowiedniej biblioteki. Ten interfejs API tworzy echo za pomocą wartości uzyskanych z interfejsów API motywu Material Design. Następnie przekaż zwrócony obiekt do komponentu Modifier.clickable lub innych komponentów.

Na przykład ten fragment kodu korzysta z wycofanych interfejsów API:

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

Zmodyfikuj ten fragment kodu, aby:

@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 pamiętać. Można go też używać wielokrotnie w wielu komponentach, podobnie jak w przypadku modyfikatorów, więc zastanów się nad wyodrębnieniem tworzonego echa z wartością najwyższego poziomu w celu zapisania przydziałów.

Wdrożenie niestandardowego systemu projektowania

Jeśli wdrażasz własny system projektowania, a do konfigurowania echa były wcześniej używane interfejs rememberRipple() i niestandardowy RippleTheme, musisz udostępnić własny interfejs ripple API, który przekazuje dostęp do interfejsów API węzła ripple ujawnionych w material-ripple. Następnie komponenty mogą korzystać z własnej echa, która bezpośrednio korzysta z wartości motywu. Więcej informacji znajdziesz w artykule Migracja z domenyRippleTheme.

Migracja z RippleTheme

Tymczasowo zrezygnuj ze zmiany działania

Biblioteki materiałów zawierają tymczasowy plik CompositionLocal, LocalUseFallbackRippleImplementation, którego możesz użyć do skonfigurowania wszystkich komponentów Material, które mają wrócić do korzystania z rememberRipple. Dzięki temu rememberRipple będzie nadal wysyłać zapytania do witryny LocalRippleTheme.

Ten fragment kodu pokazuje, jak korzystać z interfejsu API LocalUseFallbackRippleImplementation CompositionLocal:

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

Jeśli używasz niestandardowego motywu aplikacji opartego na Material, możesz bezpiecznie dodać kompozycję lokalną jako część motywu aplikacji:

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

Więcej informacji znajdziesz w sekcji Uaktualnij bibliotekę Material bez migracji.

Używanie funkcji RippleTheme do wyłączania echa w danym komponencie

Biblioteki material i material3 udostępniają RippleConfiguration i LocalRippleConfiguration, które umożliwiają skonfigurowanie wyglądu echa w obrębie drzewa podrzędnego. Pamiętaj, że funkcje RippleConfiguration i LocalRippleConfiguration są eksperymentalne i służą tylko do dostosowywania poszczególnych komponentów. Te interfejsy API nie obsługują dostosowywania globalnych ani ogólnych. Więcej informacji na ten temat znajdziesz w artykule na temat globalnego zmieniania wszystkich echa w aplikacji za pomocą RippleTheme.

Na przykład ten fragment kodu korzysta z 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 ten fragment kodu, aby:

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

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

Używanie funkcji RippleTheme do zmiany koloru/alfa fali dla danego komponentu

Jak opisano w poprzedniej sekcji, RippleConfiguration i LocalRippleConfiguration to eksperymentalne interfejsy API, przeznaczone tylko do dostosowywania w poszczególnych składnikach.

Na przykład ten fragment kodu korzysta z 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 ten fragment kodu, aby:

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

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

Używanie RippleTheme do globalnej zmiany wszystkich echa w aplikacji

Wcześniej do definiowania echa na poziomie motywu można było używać właściwości LocalRippleTheme. Ogólnie był to punkt integracji między lokalnym kompozycją systemu a falami. Zamiast ujawniać ogólny element podstawowy motywu, material-ripple udostępnia teraz funkcję createRippleModifierNode(). Ta funkcja umożliwia bibliotekom systemowym projektowania tworzenie implementacji wrapper wyższego rzędu, które wysyła zapytania do wartości motywu, a następnie przekazuje implementację echa do węzła utworzonego przez tę funkcję.

Dzięki temu systemy projektowania mogą bezpośrednio wysyłać zapytania dotyczące ich potrzeb i wyświetlać na wierzchu wszystkie wymagane warstwy motywów konfigurowanych przez użytkownika bez konieczności spełniania wymagań określonych w warstwie material-ripple. Ta zmiana dokładniej określa też motyw lub specyfikację, do której dostosowuje się Echo, ponieważ to właśnie interfejs Ripple API określa tę umowę, a nie pośrednio wywodzi się z motywu.

Wskazówki znajdziesz w artykule o implementacji interfejsu Ripple API w bibliotekach Material Design. W razie potrzeby zastąp wywołania lokalnych składów Material Design w swoim systemie projektowania.

Migracja z Indication do IndicationNodeFactory

Mija Indication

Jeśli tylko tworzysz obiekt Indication do przekazywania, na przykład tworzysz echa, która przekazuje do Modifier.clickable lub Modifier.indication, nie musisz wprowadzać żadnych zmian. IndicationNodeFactory dziedziczy dyrektywę z Indication, więc wszystko będzie nadal kompilować i działać.

Tworzę regułę zasad Indication

Jeśli tworzysz własną implementację Indication, w większości przypadków migracja powinna być prosta. Weźmy na przykład Indication, który stosuje efekt skalowania w przypadku prasy:

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. Zmień rolę ScaleIndicationInstance na DrawModifierNode. Powierzchnia interfejsu API DrawModifierNode jest bardzo podobna do IndicationInstance: udostępnia funkcję ContentDrawScope#draw(), która jest odpowiednikiem IndicationInstance#drawContent(). Musisz zmienić tę funkcję, a potem zaimplementować logikę collectLatest bezpośrednio w węźle, a nie w Indication.

    Na przykład ten fragment kodu korzysta z 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 ten fragment kodu, aby:

    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. Aby wdrożyć komponent IndicationNodeFactory, przeprowadź migrację ScaleIndication. Logika zbierania 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 korzysta z 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 ten fragment kodu, aby:

    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 zostanie utworzony za pomocą Indication

W większości przypadków, aby wyświetlić Indication dla komponentu, należy użyć metody Modifier.indication. W rzadkich przypadkach, gdy ręcznie tworzysz element IndicationInstance za pomocą rememberUpdatedInstance, musisz zaktualizować implementację, aby sprawdzić, czy Indication to IndicationNodeFactory. Pozwoli Ci to korzystać z lżejszej implementacji. Na przykład Modifier.indication będzie wewnętrznie delegować dostęp do utworzonego węzła, jeśli jest to węzeł IndicationNodeFactory. W przeciwnym razie użyje metody Modifier.composed do wywołania metody rememberUpdatedInstance.