迁移到 Indication API 和 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 库版本

如需取消屏蔽库版本的升级,您可以使用临时 LocalUseFallbackRippleImplementation CompositionLocal API 将 Material 组件配置为回退到旧行为:

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

请务必在 MaterialTheme 外部提供此信息,以便可以通过 LocalIndication 提供旧的涟漪效果。

以下部分介绍了如何迁移到新 API。

rememberRipple 迁移到 ripple

使用 Material 库

如果您使用的是 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迁移

暂时停用行为变更

Material 库具有临时的 CompositionLocal (LocalUseFallbackRippleImplementation),可用于将所有 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 在主题级定义涟漪行为。这本质上是自定义设计系统 CompositionLocal 和涟漪效果之间的集成点。现在,material-ripple 会公开 createRippleModifierNode() 函数,而不是公开通用主题基元。借助此函数,设计系统库可以创建高阶 wrapper 实现,此类实现查询其主题值,然后将涟漪实现委托给此函数创建的节点。

这样,设计系统就可以直接查询所需的内容,并在顶部公开任何必需的用户可配置主题层,而无需遵循 material-ripple 层提供的设置。这一变更也让涟漪效果所符合的主题/规范更加明确,因为涟漪效果 API 本身定义了该协定,而不是从主题隐式衍生而来。

如需相关指导,请参阅 Material 库中的 Ripple API 实现,并根据需要替换对 Material CompositionLocal 的调用。

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 Surface 与 IndicationInstance 非常相似:它公开了一个在功能上等同于 IndicationInstance#drawContent()ContentDrawScope#draw() 函数。您需要更改该函数,然后直接在节点内(而不是 Indication)实现 collectLatest 逻辑。

    例如,以下代码段使用了已废弃的 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