Compose Indication:点击效果设置
Compose Indication:打造独特点击效果的秘密武器
在Compose开发中,大家可能都碰到过Indication,不少人第一次接触它,是在想去掉Material默认的点击水波纹效果的时候。要是在AI工具里搜“怎么去掉水波纹效果”,会得到这样一段代码:
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,它能实现更高效的指示效果。Indication一般通过LocalIndication在整个层级结构中提供,开发者可以自定义LocalIndication,改变组件(比如clickable
)的默认指示效果。从这里能看出,Indication就是用来实现点击、聚焦、拖动等组件效果的,有点像传统View系统里的selector,但它的功能可要强大得多。
在传统View系统里,selector能根据组件的不同状态,指定不同的资源或颜色。不过,要实现交互效果,得知道组件的当前状态。而Indication本身没办法获取组件的交互状态,这时就需要Interaction
来帮忙了。
在很多情况下,开发普通组件时,我们不用关心Compose组件是怎么解读用户操作的。比如创建一个Button
,通过Modifier.clickable
就能判断用户有没有点击,设置好onClick
代码就行,不用管是点击屏幕还是用键盘操作。但要是想自定义组件对用户行为的响应方式,Interaction
就派上用场了。
当用户和界面组件交互时,系统会生成很多Interaction
事件。比如用户点击按钮,按钮会生成PressInteraction.Press
;在按钮范围内松开手指,会生成PressInteraction.Release
,表示点击完成;要是手指拖出按钮范围再松开,就会生成PressInteraction.Cancel
,代表点击取消。这些互动事件没有预设的含义,也不解读操作顺序和优先级。
如果想跟踪互动来扩展组件功能,比如让按钮按下时变色,最简单的办法就是观察互动状态。InteractionSource
提供了很多方法来获取各种互动状态,像调用InteractionSource.collectIsPressedAsState()
,就能知道按钮有没有被按下:
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
,又能绘制视觉效果,所以简单场景下,用Modifier.clickable
就行,不一定非要Modifier.indication
。
来看个例子,把点击缩放效果用Indication实现,步骤如下:
- 创建负责应用缩放效果的
Modifier.Node
。这个节点要观察互动来源,和之前的示例类似,但它会直接启动动画,而不是把互动转成状态。节点需要实现DrawModifierNode
,重写ContentDrawScope#draw()
,用Compose的图形API渲染缩放效果,调用drawContent()
绘制应用Indication
的组件,注意一定要调用,不然组件不会显示。
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
) {
this@draw.drawContent()
}
}
}
- 创建
IndicationNodeFactory
,它的任务就是创建新节点实例。如果没有配置参数,工厂可以是个对象:
object ScaleIndication : IndicationNodeFactory {
override fun create(interactionSource: InteractionSource): DelegatableNode {
return ScaleNode(interactionSource)
}
override fun equals(other: Any?): Boolean = other === ScaleIndication
override fun hashCode() = 100
}
Modifier.clickable
内部用了Modifier.indication
,要让组件带有ScaleIndication
的点击效果,直接把Indication
作为clickable
的参数就行:
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能定义一套交互效果并应用到各种组件上,如果项目里有标准的交互效果设计,用Indication准没错。欢迎大家一起交流,有问题可以在评论区留言或者私信我!