遷移至 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 Design 元件就不會使用這些值

下節說明如何在不遷移的情況下暫時改回舊行為,但我們建議您遷移至新的 API。如需遷移操作說明,請參閱rememberRipple 遷移至 ripple,以及後續章節。

升級 Material 程式庫版本而不遷移

如要解除封鎖升級程式庫版本,您可以使用臨時 LocalUseFallbackRippleImplementation CompositionLocal API 設定 Material 元件,改回使用舊行為:

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

請務必在 MaterialTheme 外部提供這項資訊,以便透過 LocalIndication 提供舊的漣漪效果。

以下各節將說明如何遷移至新版 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」遷移

暫時選擇不採用行為變更

Material 程式庫提供臨時 CompositionLocalLocalUseFallbackRippleImplementation,可用於設定所有 Material 元件,改回使用 rememberRipple。這樣一來,rememberRipple 會繼續查詢 LocalRippleTheme

下列程式碼片段展示如何使用 LocalUseFallbackRippleImplementation CompositionLocal API:

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

如果您使用以 Material 為基礎的自訂應用程式主題,可以安全提供本機組合做為應用程式主題的一部分:

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

詳情請參閱「在不遷移的情況下升級 Material 程式庫版本」一節。

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

您應該將上述程式碼片段修改為:

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

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) {
        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 程式庫中的 ripple 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 非常類似,也就是會公開功能相當於 IndicationInstance#drawContent()ContentDrawScope#draw() 函式。您必須變更該函式,然後直接在節點內實作 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,才能使用較淺的實作。舉例來說,如果為 IndicationNodeFactoryModifier.indication 會在內部委派給已建立的節點。如果不是,則會使用 Modifier.composed 呼叫 rememberUpdatedInstance