Compose Indication 实现点击效果

Indication是什么

多数人第一次接触 Indication,应该是为了移除 Material 默认点击水波纹效果。在 AI 工具里问:如何移除水波纹效果,你将会得到这段代码:

kotlin 复制代码
 Box(
        modifier = Modifier
            .size(200.dp)
            .clickable(
                // 去除指示效果
                indication = null, 
                interactionSource = null
            ) {
                // 点击事件处理逻辑
            },
        contentAlignment = Alignment.Center
    ) {
        Text(text = "No Ripple Click", color = Color.Black)
    }

将 indication 参数指定为 null,则移除了水波纹效果。

它是怎么做到的呢?我们先来看下 Indication 的注释

Indication 表示在某些交互发生时出现的视觉效果。例如:当组件被按下时显示涟漪效应,或者当组件被聚焦时显示高亮。 要实现自己的Indication ,请参见IndicationNodeFactory ------一个优化的指示,允许比已弃用的memorberupdatedinstance更有效的实现。 Indication 通常是通过LocalIndication在整个层次结构中提供的。你可以为LocalIndication提供一个自定义的Indication,以改变用于组件(如clickable)的默认指示。

从注释我们可以看出来,Indication 就是用来实现点击、聚焦或拖动时的组件效果。这其实就相当于是传统 View 系统下的 selector,对吧?不过 Indication 的功能要强大得多!

xml 复制代码
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true"
           android:drawable="@color/colorGreen" />
    <item android:state_pressed="false"
           android:drawable="@color/colorYellow" />
     <!-- This is the default state item, it is same as <item android:drawable="@color/colorRed" />
          This item should be written in the end. -->
     <item android:state_window_focused="true" android:drawable="@color/colorRed"/>
</selector>

selector 可以通过组件不同的状态指定不同的资源、颜色。而要实现交互的效果,那就需要知道组件的当前状态。但是,Indication 本身是没法知道组件的交互状态的。这时候我们就需要另一个工具 Interaction

Interaction「交互/互动」 是什么

在许多情况下,你无需了解 Compose 组件如何解读用户互动。例如,Button 通过 Modifier.clickable 来判断用户是否点击了按钮。如果你要在应用中添加一个普通的按钮,可以定义该按钮的 onClick 代码,而 Modifier.clickable 将会适时运行该代码。这意味着,你不必知道用户是点按了屏幕还是通过键盘选择了该按钮;Modifier.clickable 会知道用户执行了点击操作,并通过运行 onClick 代码来做出响应。

不过,如果你想自定义界面组件对用户行为的响应方式,则可能需要详细了解背后的运作原理。Interaction 就派上了用场。

当用户与界面组件互动时,系统会生成多个 Interaction 事件来表示其行为。例如,如果用户轻触某个按钮,该按钮会生成 PressInteraction.Press。如果用户在按钮范围内松开手指,系统就会生成 PressInteraction.Release,让按钮知道点击已完成。相反,如果用户将手指拖动到按钮范围外再松开,按钮就会生成 PressInteraction.Cancel,表示按下按钮的操作已取消,并未完成。

这些互动无预设立场。也就是说,这些低级别互动事件不会解读用户操作的含义或序列,也不会解读用户操作的优先顺序。

互动状态

你可以同时自行跟踪互动,以扩展组件的内置功能。例如,你可能希望某个按钮在被按下时改变颜色。最简单的互动跟踪方法就是观察相应的互动状态。**InteractionSource 提供了多种方法来获取各种互动状态。例如,如果你想查看是否按下了特定按钮,可以调用其 InteractionSource.collectIsPressedAsState() 方法:

kotlin 复制代码
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

collectIsPressedAsState() 之外,Compose 还提供了 collectIsFocusedAsState()collectIsDraggedAsState()collectIsHoveredAsState()。这些方法实际上是基于较低级别 InteractionSource API 的便捷方法。在某些情况下,建议你直接使用这些较低级别的函数。

关于Interaction 暂时就说这么多,大家感兴趣的可以自己去官网学习。此处大家只需要知道其作用就行,我们再回到 Indication

使用 Indication 创建和应用可重复使用的自定义效果

  • IndicationNodeFactory:用于创建 Modifier.Node 实例的工厂, 为组件渲染视觉效果对于 它可以是单一实例(对象),并且可以在作用整个应用

    这些实例可以是有状态实例,也可以是无状态实例。由于它们是 组件,它们可以从 CompositionLocal 检索值,以更改方式 与其他所有组件一样 Modifier.Node

  • Modifier.indication: 一个修饰符,用于绘制Indication 组件。Modifier.clickable 和其他高级互动修饰符 直接接受指示参数,这样它们不仅能发出 Interaction,但也可以为其 Interaction 绘制视觉效果 emit。因此,在简单的情况下,你可以只使用 Modifier.clickable,而不使用 需要 Modifier.indication

来看一个示例,比如将上面点击缩放的示例转为 Indication,具体步骤:

1.创建负责应用缩放效果的 Modifier.Node。 附加后,节点会观察互动来源,这与之前表示的类似 示例。唯一的区别在于,它会直接启动动画 而不是将传入的互动转换为状态。

节点需要实现 DrawModifierNode,以便可以替换 ContentDrawScope#draw(),然后使用相同的绘图渲染缩放效果 命令与 Compose 中的任何其他图形 API 相同。

调用 ContentDrawScope 接收器提供的 drawContent() 将绘制 实际应该应用 Indication 的组件,只需 需要在缩放转换中调用此函数。请确保您的 Indication 实现始终会在某个时间点调用 drawContent(); 否则,系统将不会绘制您应用 Indication 的组件。

kotlin 复制代码
private class ScaleNode(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
        ) {
            [email protected]()
        }
    }
}

2.创建 IndicationNodeFactory。它的唯一责任就是 新节点实例。由于没有 参数来配置指示,则工厂可以是对象:

kotlin 复制代码
object ScaleIndication : IndicationNodeFactory {
    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return ScaleNode(interactionSource)
    }

    override fun equals(other: Any?): Boolean = other === ScaleIndication
    override fun hashCode() = 100
}

3.Modifier.clickable 在内部使用 Modifier.indication,因此 带有 ScaleIndication 的可点击组件,您只需提供 Indication 作为 clickable 的参数

kotlin 复制代码
Box(
    modifier = Modifier
        .size(100.dp)
        .clickable(
            onClick = {},
            indication = ScaleIndication,
            interactionSource = null
        )
        .background(Color.Blue),
    contentAlignment = Alignment.Center
) {
    Text("Hello!", color = Color.White)
}

如此之后,我们便完成了一个按住之后缩放的一个交互效果,你可以把这个 Indication 应用在任何 Composable 函数上。

这就是Indication的作用,它可以定义一套交互效果然后应用到任何组件上。如果你们的交互设计有一套或多套标准的交互效果,那就不要犹豫直接使用它。

欢迎大家一起交流,有问题可以评论或私信!

相关推荐
聪明的墨菲特i23 分钟前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
时光少年24 分钟前
Android 副屏录制方案
android·前端
拉不动的猪31 分钟前
v2升级v3需要兼顾的几个方面
前端·javascript·面试
时光少年34 分钟前
Android 局域网NIO案例实践
android·前端
半兽先生1 小时前
VueDOMPurifyHTML 防止 XSS(跨站脚本攻击) 风险
前端·xss
alexhilton1 小时前
Jetpack Compose的性能优化建议
android·kotlin·android jetpack
冴羽1 小时前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
流浪汉kylin1 小时前
Android TextView SpannableString 如何插入自定义View
android
Jackson__1 小时前
面试官:谈一下在 ts 中你对 any 和 unknow 的理解
前端·typescript
zpjing~.~1 小时前
css 二维码始终显示在按钮的正下方,并且根据不同的屏幕分辨率自动调整位置
前端·javascript·html