遷移至 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:materialandroidx.compose.material3:material3androidx.wear.compose:compose-materialandroidx.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。因此,如果您在應用程式中設定 LocalRippleThemeMaterial 元件不會使用這些值

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

rememberRipple 遷移至 ripple

使用 Material Design 程式庫

如果您使用 Material 程式庫,請直接將 rememberRipple() 替換為對應程式庫的 ripple() 呼叫。這個 API 會使用從 Material 主題 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 為特定元件停用漣漪效果

materialmaterial3 程式庫會公開 RippleConfigurationLocalRippleConfiguration,方便您設定子樹狀結構中漣漪的外觀。請注意,RippleConfigurationLocalRippleConfiguration 屬於實驗功能,僅適用於自訂個別元件。這些 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 值

如上一節所述,RippleConfigurationLocalRippleConfiguration 是實驗性 API,僅適用於元件自訂。

舉例來說,下列程式碼片段使用已淘汰的 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.clickableModifier.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 遷移為 DrawModifierNodeDrawModifierNode 的 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,以便使用較輕量的導入作業。舉例來說,如果 Modifier.indicationIndicationNodeFactory,系統會在內部委派給建立的節點。否則,系統會使用 Modifier.composed 呼叫 rememberUpdatedInstance