迁移到 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 库有一个临时的 CompositionLocalLocalUseFallbackRippleImplementation,您可以使用它来配置 要回退到使用 rememberRipple 的 Material 组件。这样, 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)
    }
}

有关更多信息,请参阅在不影响 迁移部分。

使用 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 层提供的内容。这一变化也使得 明确涟漪遵循什么主题/规范,因为它是 定义该协定的 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 迁移为 DrawModifierNode。API Surface 与 IndicationInstance 非常相似:它会公开一个DrawModifierNode 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.indicationIndication 组件。不过,在极少数情况下 IndicationInstance正在使用rememberUpdatedInstance,您需要更新 实现来检查 Indication 是否为 IndicationNodeFactory,以便您 可以使用更轻量的实现例如,Modifier.indication 将 如果创建的节点为 IndicationNodeFactory,则会在内部将其委派给该节点。如果 否则,它将使用 Modifier.composed 来调用 rememberUpdatedInstance