遷移至 Indication 和 Ripple API

為提升使用 Modifier.clickable 的互動式元件組合效能,我們推出了新的 API。這些 API 可讓您更有效率地實作 Indication,例如波紋。

androidx.compose.foundation:foundation:1.7.0+ 和 androidx.compose.material:material-ripple:1.7.0+ 包含下列 API 變更:

已淘汰

取代選項

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

改用 Material 程式庫提供的新 ripple() API。

注意:在本情境中,「Material 程式庫」是指 androidx.compose.material:material、androidx.compose.material3:material3、androidx.wear.compose:compose-material 和 androidx.wear.compose:compose-material3.

RippleTheme

:

  • 使用 Material 程式庫 RippleConfiguration API,或
  • 建構自己的設計系統波紋實作

本頁說明行為變更的影響,以及遷移至新 API 的操作說明。

行為異動

下列程式庫版本包含波紋行為變更:

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

這些版本的 Material 程式庫不再使用 rememberRipple(),而是改用新的波紋 API。因此不會查詢 LocalRippleTheme。因此,如果您在應用程式中設定 LocalRippleTheme,Material 元件不會使用這些值。

以下各節將說明如何遷移至新版 API。

從 rememberRipple 遷移至 ripple

使用 Material 程式庫

如果您使用的是 Material 程式庫,請直接將 rememberRipple() 替換為對應程式庫中的 ripple() 呼叫。這個 API 會使用 Material Design 主題 API 衍生出的值建立波紋。接著,將傳回的物件傳遞至 Modifier.clickable 和/或其他元件。

舉例來說,下列程式碼片段使用已淘汰的 API:

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

請將上述程式碼片段修改為:

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

請注意,ripple() 不再是可組合函式,因此不需要記住。此外,這項功能也可以在多個元件中重複使用,類似於修飾符,因此建議您將波紋建立作業擷取至頂層值,以節省分配。

導入自訂設計系統

如果您要實作自己的設計系統,且先前使用 rememberRipple() 搭配自訂 RippleTheme 設定波紋,則應改為提供自己的波紋 API,委派給 material-ripple 中公開的波紋節點 API。接著,元件可以使用直接採用主題值的自訂漣漪效果。詳情請參閱「從RippleTheme遷移」。

從 RippleTheme 遷移

使用 RippleTheme 停用特定元件的波紋效果

material 和 material3 程式庫會公開 RippleConfiguration 和 LocalRippleConfiguration,讓您設定子樹狀結構中波紋的外觀。請注意,RippleConfiguration 和 LocalRippleConfiguration 僅適用於個別元件自訂。這些 API 不支援全域/主題範圍的自訂功能;如要進一步瞭解該用途,請參閱「使用 RippleTheme 全域變更應用程式中的所有波紋」。

舉例來說,下列程式碼片段使用已淘汰的 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 {
            // ...
        }
    }

請將上述程式碼片段修改為:

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

使用 RippleTheme 變更特定元件的波紋顏色/Alpha 值

如上一節所述,RippleConfiguration 和 LocalRippleConfiguration 僅適用於個別元件自訂。

舉例來說,下列程式碼片段使用已淘汰的 API:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

請將上述程式碼片段修改為:

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

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

使用 RippleTheme 全域變更應用程式中的所有波紋

先前,您可以使用 LocalRippleTheme 定義整個主題層級的波紋行為。這基本上是自訂設計系統組合本機值和波紋效果之間的整合點。material-ripple 現在會公開 createRippleModifierNode() 函式,而非公開一般主題設定原始物件。這項函式可讓設計系統程式庫建立更高階的 wrapper 實作,查詢其主題值,然後將波紋實作委派給這個函式建立的節點。

這樣一來,設計系統就能直接查詢所需內容,並在頂層公開任何必要的使用者可設定主題層,而不必遵循 material-ripple 層提供的內容。這項變更也更明確指出波紋符合的主題/規格,因為波紋 API 本身會定義該合約,而不是從主題隱含衍生。

如需指引,請參閱 Material 程式庫中的波紋 API 實作,並視設計系統需求,替換對 Material 組合本機值的呼叫。

從 Indication 遷移至 IndicationNodeFactory

Indication前後

如果您只是建立 Indication 來傳遞,例如建立波紋來傳遞至 Modifier.clickable 或 Modifier.indication,則不需要進行任何變更。IndicationNodeFactory 繼承自 Indication,因此所有內容都會繼續編譯並運作。

正在建立「Indication」

如果您要建立自己的 Indication 實作,在大多數情況下,遷移作業應該很簡單。舉例來說,假設 Indication 在按下時會套用縮放效果:

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

您可以透過下列兩個步驟遷移:

  1. 將 ScaleIndicationInstance 遷移為 DrawModifierNode。DrawModifierNode 的 API 介面與 IndicationInstance 非常相似:它會公開 ContentDrawScope#draw() 函式,功能等同於 IndicationInstance#drawContent()。您需要變更該函式,然後直接在節點內實作 collectLatest 邏輯,而不是 Indication。

    舉例來說,下列程式碼片段使用已淘汰的 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()
            }
        }
    }

    請將上述程式碼片段修改為:

    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. 遷移 ScaleIndication 以實作 IndicationNodeFactory。由於收集邏輯已移至節點,因此這個工廠物件非常簡單,唯一職責是建立節點執行個體。

    舉例來說,下列程式碼片段使用已淘汰的 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
        }
    }

    請將上述程式碼片段修改為:

    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 建立 IndicationInstance

在多數情況下,您應使用 Modifier.indication 顯示元件的 Indication。不過,在極少數情況下,如果您使用 rememberUpdatedInstance 手動建立 IndicationInstance,則需要更新實作,檢查 Indication 是否為 IndicationNodeFactory,以便使用較輕量的實作。舉例來說,如果建立的節點是 IndicationNodeFactory,Modifier.indication 會在內部委派給該節點。如果沒有,則會使用 Modifier.composed 呼叫 rememberUpdatedInstance。