界面组件可通过特定的互动响应方式,向设备用户提供反馈。每个组件都有自己的互动响应方式,这有助于用户了解互动如何进行。例如,如果用户轻触设备触摸屏上的某个按钮,该按钮可能会以某种方式发生变化,比如添加突出显示颜色。此变化可让用户知道他们触摸了该按钮。如果用户不想这样做,他们就会拖动手指至按钮范围外再松开,否则就会激活按钮。
Compose 手势文档介绍了 Compose 组件如何处理低级指针事件(例如指针移动和点击)。Compose 默认会将这些低级别事件抽象化处理为更高级别的互动,例如,一系列指针事件合起来可产生“按下和松开按钮”的效果。了解这类更高级别的抽象化处理,您就可以更好地自定义界面对用户的响应方式。例如,您可能想要自定义组件在与用户互动时外观如何变化,或者想要保留这些用户操作的日志。本文档为您提供了相关信息,以便您了解如何修改标准界面元素或设计自己的界面元素。
互动
在许多情况下,您无需了解 Compose 组件如何解读用户互动。例如,Button
通过 Modifier.clickable
来判断用户是否点击了按钮。如果您要在应用中添加一个普通的按钮,可以定义该按钮的 onClick
代码,而 Modifier.clickable
将会适时运行该代码。这意味着,您不必知道用户是点按了屏幕还是通过键盘选择了该按钮;Modifier.clickable
会知道用户执行了点击操作,并通过运行 onClick
代码来做出响应。
不过,如果您想自定义界面组件对用户行为的响应方式,则可能需要详细了解背后的运作原理。本部分将对此进行说明。
当用户与界面组件互动时,系统会生成多个 Interaction
事件来表示其行为。例如,如果用户轻触某个按钮,该按钮会生成 PressInteraction.Press
。如果用户在按钮范围内松开手指,系统就会生成 PressInteraction.Release
,让按钮知道点击已完成。相反,如果用户将手指拖动到按钮范围外再松开,按钮就会生成 PressInteraction.Cancel
,表示按下按钮的操作已取消,并未完成。
这些互动无预设立场。也就是说,这些低级别互动事件不会解读用户操作的含义或序列,也不会解读用户操作的优先顺序。
这些互动通常成对出现,包含起始互动和结尾互动。第二次互动包含对第一次互动的引用。例如,如果用户轻触某个按钮后松开手指,轻触操作会生成 PressInteraction.Press
互动,松开操作则会生成 PressInteraction.Release
。Release
具有 press
属性,用于识别初始 PressInteraction.Press
。
您可以观察特定组件的 InteractionSource
来查看其互动。InteractionSource
以 Kotlin Flow 为基础,因此您可以像使用任何其他数据流 (flow) 时一样从该数据流 (flow) 中收集互动。
互动状态
您可以同时自行跟踪互动,以扩展组件的内置功能。例如,您可能希望某个按钮在被按下时改变颜色。最简单的互动跟踪方法就是观察相应的互动状态。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 的便捷方法 (convenience methods)。在某些情况下,建议您直接使用这些较低级别的函数。
例如,假设您需要知道用户是否按下并拖动了按钮。如果您同时使用 collectIsPressedAsState()
和 collectIsDraggedAsState()
,Compose 会执行大量重复工作,且无法保证互动顺序正确。对于这类情况,您可能需要直接使用 InteractionSource
。下一部分将介绍您可以如何跟踪互动,并只获取所需的信息。
使用 InteractionSource
如果您需要低级别的组件互动信息,可以为该组件的 InteractionSource
使用标准 Flow API。例如,假设您想要维护 InteractionSource
的按下和拖动互动列表。以下代码会执行一半的工作,在发生新的按下互动时立即将其添加到列表中:
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> {
interactions.add(interaction)
}
is DragInteraction.Start -> {
interactions.add(interaction)
}
}
}
}
但是,除了添加新的互动之外,您还必须在互动结束(例如,用户将手指从组件上松开)时移除互动。这很简单,因为结尾互动一律带有对关联的起始互动的引用。以下代码展示了如何移除已结束的互动:
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> {
interactions.add(interaction)
}
is PressInteraction.Release -> {
interactions.remove(interaction.press)
}
is PressInteraction.Cancel -> {
interactions.remove(interaction.press)
}
is DragInteraction.Start -> {
interactions.add(interaction)
}
is DragInteraction.Stop -> {
interactions.remove(interaction.start)
}
is DragInteraction.Cancel -> {
interactions.remove(interaction.start)
}
}
}
}
现在,如果您想知道该组件目前是否处于按下或拖动状态,只需检查 interactions
是否为空即可:
val isPressedOrDragged = interactions.isNotEmpty()
如果您想知道最近一次互动是什么,只需查看列表中的最后一项即可。例如,以下代码展示了 Compose 水波纹效果在执行时如何确定最近一次互动适用的状态遮罩层:
val lastInteraction = when (interactions.lastOrNull()) {
is DragInteraction.Start -> "Dragged"
is PressInteraction.Press -> "Pressed"
else -> "No state"
}
示例说明
如需了解如何构建包含自定义输入响应的组件,请参考以下修改后的按钮示例。在此示例中,假设您想在用户按下按钮时使按钮外观发生变化:
为此,请基于 Button
构建自定义可组合项 (composable),并让其利用额外的 icon
参数来绘制图标(在本例中为购物车图标)。您需要调用 collectIsPressedAsState()
来跟踪用户手指是否悬停在按钮上;如果悬停在按钮上,则添加图标。代码如下所示:
@Composable
fun PressIconButton(
onClick: () -> Unit,
icon: @Composable () -> Unit,
text: @Composable () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource =
remember { MutableInteractionSource() },
) {
val isPressed by interactionSource.collectIsPressedAsState()
Button(onClick = onClick, modifier = modifier,
interactionSource = interactionSource) {
AnimatedVisibility(visible = isPressed) {
if (isPressed) {
Row {
icon()
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
}
}
}
text()
}
}
使用新的可组合项,如下所示:
PressIconButton(
onClick = {},
icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
text = { Text("Add to cart") }
)
由于这个新的 PressIconButton
以现有 Material Button
为基础,因此会照常响应用户互动。当用户按下按钮时,按钮的不透明度会稍微改变,就像一般的 Material Button
一样。此外,由于这部分新代码,HoverIconButton
还会通过添加图标来动态响应悬停操作。