# 手势系统

业务开发中,我们常需要监听节点 touch 事件,处理拖拽、缩放相关逻辑。由于 Skyline 采用双线程架构,在进行这样的交互动画时,会具有较大的异步延迟,这点可以参考 wxs 响应事件

Skylinewxs 代码运行在 JS 线程,而事件产生在 UI 线程,因此 wxs 动画 性能有所降低,为了提升小程序交互体验的效果,我们内置了一批手势组件,使用手势组件的优势包括

  1. 免去开发者监听 touch 事件,自行计算手势逻辑的复杂步骤
  2. 手势组件直接在 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 节点添加了两种类型的手势监听。

  1. 当在屏幕上横向滑动时,horizontal-drag 手势节点的回调将被触发;
  2. 当在屏幕上纵向滑动时,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 手势识别成功的回调
on-gesture-event eventhandler worklet:ongesture 的旧名,即将废弃
worklet:should-response-on-move callback 手指移动过程中手势是否响应
should-response-on-move callback worklet:should-response-on-move 的旧名,即将废弃
worklet:should-accept-gesture callback 手势是否应该被识别
should-accept-gesture callback worklet:should-accept-gesture 的旧名,即将废弃
simultaneous-handlers Array<string> [] 声明可同时触发的手势节点
native-view string 代理的原生节点类型

native-view 支持的枚举值有 scroll-viewswiper。滚动容器纵向滚动时,使用 <vertical-drag-gesture-handler> 手势组件代理内部手势,横向滚动时,则使用 <horizontal-drag-gesture-handler>

  • eventhandler 类型是事件回调,无返回值
  • callback 类型是开发者注册到组件的回调函数,会在适当时机被执行以读取返回值
  • 所有的回调都只能传入一个 worklet 回调

# 事件回调参数

不同类型手势组件 on-gesture-event 返回的参数如下

# 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 跟踪的手指数
  • 多指滑动时,focalXfocalY 为多个触摸点中心焦点的坐标
  • 单指滑动时,pointerCount = 1,此时效果同 pan-gesture-handlerscale 手势是 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 压力大小

# 手势状态

所有手势 on-gesture-event 回调均会返回一个 state 状态字段。

enum State {
  // 手势未识别
  POSSIBLE = 0,
  // 手势已识别
  BEGIN = 1,
  // 连续手势活跃状态
  ACTIVE = 2,
  // 手势终止
  END = 3,
  // 手势取消
  CANCELLED = 4,
}

我们将手势分为如下两种类型:

  1. 离散手势:tapdouble-tap,仅触发一次
  2. 连续手势:其它类型的手势组件,随手指拖动会触发多次

tap-gesture-handler 手势组件返回的 state 始终为 1。

pan-gesture-handler 手势组件在一个完整的拖动过程中,state 会按如下方式改变

  1. 手指刚接触屏幕时,state = 0
  2. 移动一小段距离,pan 手势判定生效时,state = 1
  3. 继续移动,state = 2
  4. 手指离开屏幕 state = 3

由于嵌套的手势会产生冲突(仅有一个最终判定识别生效),因此连续手势 state 的变化可能有如下一些情景,开发者需要根据 state 值来处理一些异常情况。

  1. POSSIBLE -> BEGIN -> ACTIVE -> END 正常流程
  2. POSSIBLE -> BEGIN -> ACTIVE -> CANCELLED 提前中断
  3. POSSIBLE -> CANCELLED 手势未识别

并不是所有的连续手势均有 POSSIBLE 状态,如 scale-gesture-handler 手势组件,当双指触摸后松手,state 变化如下:

  1. 双指触摸屏幕,state = 1, pointerCount = 2
  2. 双指放大操作,state = 2, pointerCount = 2
  3. 双指离开屏幕,state = 3, pointerCount = 1,之后会相继回调 a. state = 1, pointerCount = 1 b. state = 2, pointerCount = 1 c. state = 3, pointerCount = 0

# 注意事项

  • 手势组件仅在 Skyline 渲染模式下才能使用
  • 手势组件为虚组件,不会进行布局,手势组件上设置 styleclass 是无效的
  • 手势组件仅能含有一个直接子节点,否则不生效
  • 手势组件的父组件样式会直接影响其子节点
  • 手势组件的回调函数均需声明为 worklet 函数
  • 手势不同于普通 touch 事件,不会进行冒泡
  • 手势组件的 eventhandler / callback 均需声明为 worklet 函数,回调在 UI 线程触发

# 使用方法

# 示例代码

在开发者工具中预览效果

# 示例一:监听拖动手势

<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> 这样的滚动容器,内部也是基于手势来处理滚动操作的。相比于 webskyline 提供了更底层的访问机制,使得在做一些复杂交互时,可以做到更细粒度、分阶段的控制。

<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 事件回调均会触发,它们的区别在于:

  1. scroll 事件仅在滚动时触发,当触顶/底后,不再回调
  2. 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>

此时,outerinner 手势组件的 on-gesture-event 回调会依次触发,结合上面提到的手势控制原理,可以实现预期的效果。完整代码参考示例 demo