# 手势系统
业务开发中,我们常需要监听节点 touch
事件,处理拖拽、缩放相关逻辑。由于 Skyline
采用双线程架构,在进行这样的交互动画时,会具有较大的异步延迟,这点可以参考 wxs 响应事件。
Skyline
中 wxs
代码运行在 JS
线程,而事件产生在 UI
线程,因此 wxs 动画
性能有所降低,为了提升小程序交互体验的效果,我们内置了一批手势组件,使用手势组件的优势包括
- 免去开发者监听
touch
事件,自行计算手势逻辑的复杂步骤 - 手势组件直接在
UI
线程响应,避免了传递到JS
线程带来的延迟
# 效果展示
下图演示了使用手势、协商手势实现的拖动小球,半屏弹窗手势拖动关闭,分段半屏等效果。点击查看更多 Skyline 示例。
扫码小程序示例,分别体验 基础手势
和 协商手势
新特性
# 手势组件
组件名称 | 触发时机 |
---|---|
<tap-gesture-handler> | 点击时触发 |
<double-tap-gesture-handler> | 双击时触发 |
<scale-gesture-handler> | 多指缩放时触发 |
<force-press-gesture-handler> | iPhone 设备重按时触发 |
<pan-gesture-handler> | 拖动(横向/纵向)时触发 |
<vertical-drag-gesture-handler> | 纵向滑动时触发 |
<horizontal-drag-gesture-handler> | 横向滑动时触发 |
<long-press-gesture-handler> | 长按时触发 |
# 工作原理
手势组件为虚组件,真正响应事件的是其直接子节点。下方代码中,我们给 container
节点添加了两种类型的手势监听。
- 当在屏幕上横向滑动时,
horizontal-drag
手势节点的回调将被触发; - 当在屏幕上纵向滑动时,
vertical-drag
手势节点的回调将被触发。
<horizontal-drag-gesture-handler>
<vertical-drag-gesture-handler>
<view id="container"></view>
</vertical-drag-gesture-handler>
</horizontal-drag-gesture-handler>
触摸屏幕时,渲染引擎会从内到外对手势监听器进行手势识别,当某个手势监听器满足条件时,其余的手势监听器将会失效。如在 scroll-view
内部添加纵向的手势监听时,将会阻断 scroll-view
内的手势监听器,导致无法滑动。
<scrol-view>
<vertical-drag-gesture-handler>
<view id="container"></view>
</vertical-drag-gesture-handler>
</scroll-view>
需要注意的是,pan
类型的判定条件比 vertical-drag
要宽松,因此纵向滑动时,vertical-drag
将会响应,而 pan
则会失效。当横向滑动时,pan
类型则会响应。
<vertical-drag-gesture-handler>
<pan-gesture-handler>
<view id="container"></view>
</pan-gesture-handler>
</vertical-drag-gesture-handler>
# 通用属性
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
tag | string | 无 | 否 | 声明手势协商时的组件标识 |
worklet:ongesture | eventhandler | 无 | 否 | 手势处理回调 |
worklet:should-response-on-move | callback | 无 | 否 | 手指移动过程中手势是否响应 |
worklet:should-accept-gesture | callback | 无 | 否 | 手势是否应该被识别 |
simultaneous-handlers | Array<string> | [] | 否 | 声明可同时触发的手势节点 |
native-view | string | 无 | 否 | 代理的原生节点类型 |
native-view
支持的枚举值有 scroll-view
和 swiper
。滚动容器纵向滚动时,使用 <vertical-drag-gesture-handler>
手势组件代理内部手势,横向滚动时,则使用 <horizontal-drag-gesture-handler>
。
eventhandler
类型是事件回调,无返回值callback
类型是开发者注册到组件的回调函数,会在适当时机被执行以读取返回值- 所有的回调都只能传入一个 worklet 回调
# 事件回调参数
# worklet:should-response-on-move
返回的参数 pointerEvent
各字段如下。每次触摸移动时进行回调,返回 false
时,则对应的手势组件无法收到该次 move
事件。
属性 | 类型 | 说明 |
---|---|---|
identifier | number | Touch 对象的唯一标识符 |
type | string | 事件类型 |
deltaX | number | 相对上一次,X 轴方向移动的坐标 |
deltaY | number | 相对上一次,Y 轴方向移动的坐标 |
clientX | number | 触点相对于可见视区左边缘的 X 坐标 |
clientY | number | 触点相对于可见视区上边缘的 Y 坐标 |
radiusX | number | 返回能够包围接触区域的最小椭圆的水平轴 (X 轴) 半径 |
radiusY | number | 返回能够包围接触区域的最小椭圆的垂直轴 (Y 轴) 半径 |
rotationAngle | number | 返回一个角度值,表示上述由radiusX 和 radiusY 描述的椭圆为了尽可能精确地覆盖用户与平面之间的接触区域而需要顺时针旋转的角度 |
force | number | 用户对触摸平面的压力大小 |
timeStamp | number | 事件触发的时间戳 |
Page({
shouldResponseOnMove(pointerEvent) {
'worklet'
return false
}
})
# worklet:should-accept-gesture
用法如下,框架手势识别生效时进行回调,由开发者决定手势是否生效。以 Pan
手势为例。
手指触摸屏幕时进入 State.Possible
状态,shouldAcceptGesture
返回 false
后进入 State.CANCELLED
状态,返回 true
后进入 State.Begin
状态,可继续接收手续 move
事件。
Page({
shouldAcceptGesture() {
'worklet'
return false
}
})
# worklet:ongesture
不同类型手势组件返回的参数如下
# tap / double-tap
属性 | 类型 | 说明 |
---|---|---|
state | number | 手势状态 |
absoluteX | number | 相对于全局的 X 坐标 |
absoluteY | number | 相对于全局的 Y 坐标 |
# pan / vertical-drag / horizontal-drag
属性 | 类型 | 说明 |
---|---|---|
state | number | 手势状态 |
absoluteX | number | 相对于全局的 X 坐标 |
absoluteY | number | 相对于全局的 Y 坐标 |
deltaX | number | 相对上一次,X 轴方向移动的坐标 |
deltaY | number | 相对上一次,Y 轴方向移动的坐标 |
velocityX | number | 手指离开屏幕时的横向速度(pixel per second) |
velocityY | number | 手指离开屏幕时的纵向速度(pixel per second) |
# scale
属性 | 类型 | 说明 |
---|---|---|
state | number | 手势状态 |
focalX | number | 中心点相对于全局的 X 坐标 |
focalY | number | 中心点相对于全局的 Y 坐标 |
focalDeltaX | number | 相对上一次,中心点在 X 轴方向移动的坐标 |
focalDeltaY | number | 相对上一次,中心点在 Y 轴方向移动的坐标 |
scale | number | 放大或缩小的比例 |
horizontalScale | number | scale 的横向分量 |
verticalScale | number | scale 的纵向分量 |
rotation | number | 旋转角(单位:弧度) |
velocityX | number | 手指离开屏幕时的横向速度(pixel per second) |
velocityY | number | 手指离开屏幕时的纵向速度(pixel per second) |
pointerCount | number | 跟踪的手指数 |
- 多指滑动时,
focalX
和focalY
为多个触摸点中心焦点的坐标 - 单指滑动时,
pointerCount = 1
,此时效果同pan-gesture-handler
,scale
手势是pan
的超集。
# long-press
属性 | 类型 | 说明 |
---|---|---|
state | number | 手势状态 |
absoluteX | number | 相对于全局的 X 坐标 |
absoluteY | number | 相对于全局的 Y 坐标 |
translationX | number | 相对于初始触摸点的 X 轴偏移量 |
translationY | number | 相对于初始触摸点的 Y 轴偏移量 |
velocityX | number | 手指离开屏幕时的横向速度(pixel per second) |
velocityY | number | 手指离开屏幕时的纵向速度(pixel per second) |
# force-press
属性 | 类型 | 说明 |
---|---|---|
state | number | 手势状态 |
absoluteX | number | 相对于全局的 X 坐标 |
absoluteY | number | 相对于全局的 Y 坐标 |
pressure | number | 压力大小 |
# 手势状态
所有手势 worklet:ongesture
回调均会返回一个 state
状态字段。
enum State {
// 手势未识别
POSSIBLE = 0,
// 手势已识别
BEGIN = 1,
// 连续手势活跃状态
ACTIVE = 2,
// 手势终止
END = 3,
// 手势取消
CANCELLED = 4,
}
我们将手势分为如下两种类型:
- 离散手势:
tap
和double-tap
,仅触发一次 - 连续手势:其它类型的手势组件,随手指拖动会触发多次
tap-gesture-handler
手势组件返回的 state
始终为 1。
pan-gesture-handler
手势组件在一个完整的拖动过程中,state
会按如下方式改变
- 手指刚接触屏幕时,
state = 0
- 移动一小段距离,
pan
手势判定生效时,state = 1
- 继续移动,
state = 2
- 手指离开屏幕
state = 3
由于嵌套的手势会产生冲突(仅有一个最终判定识别生效),因此连续手势 state
的变化可能有如下一些情景,开发者需要根据 state
值来处理一些异常情况。
POSSIBLE -> BEGIN -> ACTIVE -> END
正常流程POSSIBLE -> BEGIN -> ACTIVE -> CANCELLED
提前中断POSSIBLE -> CANCELLED
手势未识别
并不是所有的连续手势均有 POSSIBLE
状态,如 scale-gesture-handler
手势组件,当双指触摸后松手,state
变化如下:
- 双指触摸屏幕,
state = 1, pointerCount = 2
- 双指放大操作,
state = 2, pointerCount = 2
- 双指离开屏幕,
state = 3, pointerCount = 1
,之后会相继回调 a.state = 1, pointerCount = 1
b.state = 2, pointerCount = 1
c.state = 3, pointerCount = 0
# 注意事项
- 手势组件仅在
Skyline
渲染模式下才能使用 - 手势组件为虚组件,不会进行布局,手势组件上设置
style
、class
是无效的 - 手势组件仅能含有一个直接子节点,否则不生效
- 手势组件的父组件样式会直接影响其子节点
- 手势组件的回调函数均需声明为
worklet
函数 - 手势不同于普通
touch
事件,不会进行冒泡 - 手势组件的
eventhandler / callback
均需声明为worklet
函数,回调在UI
线程触发
# 使用方法
# 示例代码
# Chaining API init 函数示例代码
# 示例一:监听拖动手势
<pan-gesture-handler on-gesture-event="handlePan">
<view></view>
</pan-gesture-handler>
Page({
handlePan(evt) {
"worklet";
console.log(evt.translateX);
},
});
# 示例二:监听嵌套的手势
<horizontal-drag-gesture-handler on-gesture-event="handleHorizontalDrag">
<vertical-drag-gesture-handler on-gesture-event="handleVerticalDrag">
<view class="circle">one-way drag</view>
</vertical-drag-gesture-handler>
</horizontal-drag-gesture-handler>
# 示例三:代理原生组件内部手势
对于 <scroll-view>
和 <swiper>
这样的滚动容器,内部也是基于手势来处理滚动操作的。相比于 web
,skyline
提供了更底层的访问机制,使得在做一些复杂交互时,可以做到更细粒度、分阶段的控制。
<vertical-drag-gesture-handler
native-view="scroll-view"
should-response-on-move="shouldScrollViewResponse"
should-accept-gesture="shouldScrollViewAccept"
on-gesture-event="handleGesture"
>
<scroll-view
scroll-y
type="list"
adjust-deceleration-velocity="adjustDecelerationVelocity"
bindscroll="handleScroll"
>
<view class="item" wx:for="{{list}}">
<view class="avatar" />
<view class="comment" />
</view>
</scroll-view>
</vertical-drag-gesture-handler>
以纵向滚动的 <scroll-view>
为例,可使用 <vertical-drag-gesture-handler>
手势组件,并声明 native-view="scroll-view"
来代理其内部手势。
# 滚动事件
当滚动列表时,手势组件的事件回调和 <scroll-view>
的 scroll
事件回调均会触发,它们的区别在于:
scroll
事件仅在滚动时触发,当触顶/底后,不再回调on-gesture-event
手势回调当手指在屏幕上滑动时会一直触发,直到松手
# 手势控制
在前面介绍连续手势状态时,我们知道手势有自身的识别过程。例如 vertical-drag
手势,当手指触摸时为 POSSIBLE
状态,移动一小段距离后才识别为 BEGIN
状态,此时称手势被识别(ACCEPT
)。
# 1. 手势识别
should-accept-gesture
属性允许开发者注册一个 callback
,并返回一个布尔值,参与到手势识别的过程。当返回 false
时,本次触摸手势不再生效,相关联的 <scroll-view>
组件也无法滚动。
# 2. 事件派发
should-response-on-move
属性允许开发者注册一个 callback
,并返回一个布尔值,参与到事件派发的过程。当返回 false
时,当次 move
的事件不再派发,相关联的 <scroll-view>
不继续滚动。该回调在手指移动过程中会持续触发,可随时改变,进而控制滚动容器继续/暂停滚动。
Page({
// 这里返回 false,则 scroll-view 无法滚动
// should-accept-gesture 会在手势识别的一开始触发一次
// should-response-on-move 是在 move 过程中不断触发
shouldScrollViewAccept() {
'worklet'
return true
},
// 这里返回 false,则 scroll-view 无法滚动
shouldScrollViewResponse(pointerEvent) {
'worklet';
return true;
},
// 手指滑动离开滚动组件时,指定衰减速度
adjustDecelerationVelocity(velocity) {
'worklet';
return velocity;
},
// scroll-view 滚动到边界后,手指滑动,scroll 事件不再触发
handleScroll(evt) {
'worklet';
},
// scroll-view 滚动到边界后,手指滑动,手势回调仍然会触发
handleGesture(evt) {
'worklet'
},
});
# 示例四:手势协商
一些场景下,我们会遇到手势冲突。如下代码所示,存在嵌套的 <vertical-drag-gesture-handler>
组件,我们希望 outer
手势组件来处理纵向的拖动,inner
手势组件处理列表的滚动,但实际仅 inner
的手势回调会触发。
嵌套的同类型手势组件,当内层的手势识别后,外层的手势组件将不会被识别。
<vertical-drag-gesture-handler tag="outer">
<vertical-drag-gesture-handler tag="inner" native-view="scroll-view">
<scroll-view scroll-y></scroll-view>
</vertical-drag-gesture-handler>
</vertical-drag-gesture-handler>
但上述场景又是很常见的,例如视频号的评论列表,列表的滚动和整个评论区的拖动衔接的十分流畅。手势协商机制用于解决该类问题,使用上也十分简单,simultaneous-handlers
属性声明多个手势可同时触发。
<vertical-drag-gesture-handler tag="outer" simultaneous-handlers="{{["inner"]}}">
<vertical-drag-gesture-handler tag="inner" simultaneous-handlers="{{["outer"]}}" native-view="scroll-view">
<scroll-view scroll-y></scroll-view>
</vertical-drag-gesture-handler>
</vertical-drag-gesture-handler>
此时,outer
和 inner
手势组件的 on-gesture-event
回调会依次触发,结合上面提到的手势控制原理,可以实现预期的效果。完整代码参考示例 demo。