有几个术语和概念需要了解 。本页介绍了 指针、指针事件和手势,并引入了不同的抽象化机制, 以及手势操作。还更深入地介绍了事件消费 传播。
定义
要了解本页中的各种概念,您需要了解一些 所用的术语:
- 指针:可用于与应用交互的实体对象。
在移动设备上,最常见的指针是手指与
触摸屏。或者,您也可以使用触控笔来代替手指。
对于大屏幕,您可以使用鼠标或触控板间接与
显示屏上。输入设备必须能够“指向”坐标为
被视为指针,因此不能将键盘等视为
指针。在 Compose 中,使用
PointerType
。 - 指针事件:描述一个或多个指针的低级交互
与应用共享的时间。任何指针交互,如将
手指在屏幕上或拖动鼠标均可触发事件。在
Compose 中,此类事件的所有相关信息都包含在
PointerEvent
类。 - 手势:可解读为单个事件的一系列指针事件 操作。例如,点按手势可被视为一系列向下的 事件,然后触发 up 事件。还有一些常用的手势 应用,例如点按、拖动或转换,但您也可以自行创建自定义 手势。
不同的抽象级别
Jetpack Compose 为处理手势提供了不同的抽象级别。
顶层是组件支持。Button
等可组合项
自动包含手势支持向自定义项添加手势支持
组件,您可以向任意控件添加手势修饰符(如 clickable
)
可组合项。最后,如果您需要自定义手势,可以使用
pointerInput
修饰符。
一般而言,应在提供
所需的全部功能这样,您就能从本演示文稿中介绍的最佳做法中
所处的位置。例如,Button
包含更多语义信息,
与 clickable
相比,后者包含的信息比原始
pointerInput
实现。
组件支持
Compose 中的许多开箱即用组件都包含某种内部手势
处理。例如,LazyColumn
会通过以下方式响应拖动手势:
滚动内容时,Button
会在您按下它时显示涟漪,
SwipeToDismiss
组件包含用于关闭
元素。此类手势处理会自动运行。
除了内部手势处理之外,许多组件还要求调用方
处理手势。例如,Button
会自动检测点按操作
并触发点击事件您将 onClick
lambda 传递给 Button
,以
对手势做出反应。同样,您将 onValueChange
lambda 添加到
Slider
,对用户拖动滑块手柄做出反应。
如果适合您的应用场景,则应首选组件中包含的手势,
包含对焦点和无障碍功能的开箱即用型支持,并且
经过充分测试。例如,以特殊方式标记 Button
,以便
无障碍服务正确将其描述为按钮,而不只是按钮
可点击元素:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
如需详细了解 Compose 中的无障碍功能,请参阅无障碍功能 写邮件。
使用修饰符向任意可组合项添加特定手势
您可以将手势修饰符应用于任意可组合项,
可组合监听手势例如,您可以让通用 Box
处理点按手势:将其设为 clickable
;或者让 Column
通过应用 verticalScroll
来处理垂直滚动。
我们提供许多修饰符来处理不同类型的手势:
- 使用
clickable
处理点按和按下操作,combinedClickable
、selectable
、toggleable
和triStateToggleable
修饰符。 - 使用
horizontalScroll
处理滚动,verticalScroll
以及更通用的scrollable
修饰符。 - 使用
draggable
和swipeable
处理拖动操作 修饰符。 - 处理多点触控手势,例如平移、旋转和缩放
transformable
修饰符。
通常,相较于自定义手势处理,建议选择开箱即用的手势修饰符。
除了纯指针事件处理之外,这些修饰符还添加了更多功能。
例如,clickable
修饰符不仅增加了对按下动作的检测和
但也会添加语义信息、关于互动的视觉指示、
悬停、焦点和键盘支持。您可以查看源代码
(共 clickable
页),了解相应功能
正在添加。
使用 pointerInput
修饰符向任意可组合项添加自定义手势
并非所有手势都是通过开箱即用的手势修饰符实现的。对于
例如,您不可以使用修饰符来响应长按、
按住 Ctrl 键并点击,或者用三指点按您可以自行编写手势
处理程序来识别这些自定义手势。您可以使用
pointerInput
修饰符,可让您访问原始指针
事件。
以下代码会监听原始指针事件:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
如果拆分此代码段,核心组件包括:
pointerInput
修饰符。您向它传递一个或多个键。当 其中一个键的值发生变化,那么辅助键内容 lambda 就是 已重新执行。该示例会向可组合项传递一个可选过滤条件。如果 该过滤器的值发生更改时,指针事件处理程序应为 以确保记录正确的事件。awaitPointerEventScope
会创建一个协程作用域,可用于 等待指针事件awaitPointerEvent
会挂起协程,直到发生下一个指针事件 。
尽管监听原始输入事件的功能强大,但编写代码也很复杂 自定义手势。为了简化自定义 也有许多实用方法可供选择。
检测完整手势
您可以监听特定手势,而无需处理原始指针事件
发生并做出适当的响应。AwaitPointerEventScope
提供
用于监听:
- 按住、点按、点按两次和长按:
detectTapGestures
- 拖动:
detectHorizontalDragGestures
,detectVerticalDragGestures
、detectDragGestures
和detectDragGesturesAfterLongPress
- 转换:
detectTransformGestures
这些是顶级检测器,因此您无法在同一位置添加多个检测器
pointerInput
修饰符。以下代码段仅检测点按,而不会检测
拖动:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
在内部,detectTapGestures
方法会屏蔽协程;第二个
检测器。如果您需要添加多个手势监听器,
可组合项,请改用单独的 pointerInput
修饰符实例:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
每个手势处理事件
根据定义,手势从按下指针事件开始。您可以使用
awaitEachGesture
辅助方法,而不是 while(true)
循环,
并传递每个原始事件。awaitEachGesture
方法会重启
包含块,表示相应手势是
已完成:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
在实践中,除非awaitEachGesture
无需识别手势即可响应指针事件。例如,
hoverable
:不会响应指针按下或释放事件,只是
需要知道指针何时进入或离开其边界。
等待特定事件或子手势
有一组方法有助于识别手势的常见部分:
- 挂起,直到通过
awaitFirstDown
按下指针,或者等待所有 点击waitForUpOrCancellation
即可向上移动。 - 使用
awaitTouchSlopOrCancellation
创建低级拖动监听器 和awaitDragOrCancellation
。手势处理程序首先挂起,直到 指针到达触摸溢出值,然后挂起,直到出现第一个拖动事件 就会出现这种错误如果您只想沿单轴拖动,请使用awaitHorizontalTouchSlopOrCancellation
以上awaitHorizontalDragOrCancellation
或awaitVerticalTouchSlopOrCancellation
以上awaitVerticalDragOrCancellation
。 - 暂停,直到长按
awaitLongPressOrCancellation
为止。 - 使用
drag
方法持续监听拖动事件,或horizontalDrag
或verticalDrag
,用于在一个设备上监听拖动事件 轴。
计算多接触点事件
当用户使用多个指针执行多点触控手势时,
基于原始值理解所需的转换十分复杂。
如果使用 transformable
修饰符或 detectTransformGestures
方法没有针对用例提供足够精细的控制,您可以
监听原始事件并对其应用计算。这些辅助方法
是calculateCentroid
、calculateCentroidSize
、
calculatePan
、calculateRotation
和 calculateZoom
。
事件调度和点击测试
并非所有指针事件都会发送到每个 pointerInput
修饰符。事件
调度的工作原理如下:
- 指针事件会分派给一个可组合项层次结构。 新指针触发其第一个指针事件时,系统开始点击测试 “有效”可组合项。可组合项符合以下条件时即被视为符合条件 指针输入处理功能。界面顶部的点击测试流程 放在底部。可组合项是“命中”当指针事件发生时 该可组合项的边界内。此过程会生成一系列 可组合项。
- 默认情况下,如果多个符合条件的可组合项位于同一级别上,
则只有 Z-index 最高的可组合项才是“hit”。对于
例如,当您向
Box
添加两个重叠的Button
可组合项时, 上面绘制的那条消息会收到所有指针事件从理论上讲, 您可以通过创建自己的PointerInputModifierNode
来替换此行为。 并将sharePointerInputWithSiblings
设置为 true。 - 同一指针的后续事件也会分派到 可组合项和数据流(根据事件传播逻辑)。系统 不对此指针执行更多点击测试。也就是说, 链中的可组合函数会接收该指针的所有事件,即使 这些事件发生在该可组合项的边界之外。不支持的可组合项 链中的所有事件均从不接收指针事件,即使指针 不超出其边界内
由鼠标或触控笔悬停时触发的悬停事件 定义的所有规则悬停事件会发送到其遇到的任何可组合项。因此 当用户将指针从一个可组合项的边界悬停到下一个可组合项的边界上时, 系统不会将事件发送到第一个可组合项,而是会发送到 新的可组合项。
事件消耗
如果有多个可组合项分配有手势处理程序,这些可组合项 两个处理程序不应发生冲突。例如,看一下以下界面:
当用户点按书签按钮时,按钮的 onClick
lambda 会处理该书签
手势。当用户点按列表项的任何其他部分时,ListItem
会处理该手势并导航到该文章。对于指针输入
Button 必须使用此事件,以便其父项知道不要使用
对它做出反应了开箱即用组件中包含的手势,以及
常用的手势修饰符都包含此使用行为,
编写自己的自定义手势,则必须手动处理事件。您需要执行此操作
使用 PointerInputChange.consume
方法:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
使用事件不会停止事件传播到其他可组合项。答 可组合项需要明确忽略已使用的事件。撰写内容时 自定义手势时,您应检查某个事件是否已被另一个事件使用 元素:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
事件传播
如前所述,指针更改会传递给它所命中的每个可组合项。
但是,如果存在多个此类可组合项,事件按什么顺序执行
传播?以上一部分中的示例为例,此界面将转换为
以下界面树,其中只有 ListItem
和 Button
响应
指针事件:
在三次运行期间,指针事件会流经每个可组合项三次 "通过":
- 在初始传递中,事件从界面树顶部流向
底部。此流程允许父级在子级之前拦截事件
消耗掉。例如,提示需要拦截
长按,而不是将其传递给子级。在我们的
例如,
ListItem
会在Button
之前收到事件。 - 在主通道中,事件从界面树的叶节点流向
是界面树的根目录这个阶段是你通常使用手势的阶段,
在监听事件时默认传递。在此卡券中处理手势
这意味着叶节点优先于其父节点,即
大多数手势的逻辑行为。在我们的示例中,
Button
收到 在ListItem
之前触发事件。 - 在 Final Pass 中,事件从界面顶部再次流动 传递给叶节点。该流程允许堆栈中较高级别的元素 响应其父级使用的事件。例如,按钮移除 其涟漪指示。
从直观上看,事件流可表示如下:
使用输入更改后,这些信息就会从 下一个步骤:
在代码中,您可以指定感兴趣的卡券:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
在此代码段中,每个 这些 await 方法调用,尽管关于消耗的数据 已更改。
测试手势
在测试方法中,您可以使用
performTouchInput
方法。这样,您就可以执行
完整手势(例如双指张合或长按)或低级手势(例如
将光标移动一定像素数):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
如需查看更多示例,请参阅 performTouchInput
文档。
了解详情
如需详细了解 Jetpack Compose 中的手势,请参阅以下文档 资源:
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- Compose 中的无障碍功能
- 滚动
- 点按并按住