移动端触摸手势库 AlloyFinger,配上 Vue 的
v-finger指令,让「点、滑、捏、转」都能用声明式写法搞定,一起看看吧。
一、为什么需要 AlloyFinger?
在 H5 里,原生 touchstart / touchmove / touchend 只能告诉你「手指动了」,至于用户是单击、双击、长按、滑动、双指缩放还是旋转,都要自己算时间差、距离、角度------既难写又容易出 bug。
AlloyFinger 是腾讯 AlloyTeam 开源的轻量级手势库,把这些常见手势都封装好了,并且提供了 Vue 插件 ,以自定义指令 v-finger 的形式在模板里绑定,写法清晰、易维护。
二、安装依赖
在项目根目录执行:
bash
npm install alloyfinger
三、在入口文件中注册插件
在 Vue 入口文件 (如 src/main.js)中做两件事:
- 引入 AlloyFinger 本体和其 Vue 插件;
- 使用
Vue.use(AlloyFingerPlugin, { AlloyFinger })注册。
这样全局就可以在任意组件的模板里使用 v-finger 指令。
javascript
// 引入 alloy-finger
import AlloyFinger from 'alloyfinger'
import AlloyFingerPlugin from 'alloyfinger/vue/alloy_finger_vue'
Vue.use(AlloyFingerPlugin, {
AlloyFinger
})
注意:
- 插件路径是
alloyfinger/vue/alloy_finger_vue - 必须把 AlloyFinger 通过
Vue.use的第二个参数传进去,插件内部会用它来创建手势实例。
四、在模板里使用 v-finger
注册完成后,在任意 Vue 组件的模板中,给需要绑定手势的单个根元素 写上 v-finger:事件名="方法名" 即可。
4.1 语法形式
html
<div
v-finger:tap="onTap"
v-finger:swipe="onSwipe"
v-finger:long-tap="onLongTap"
>
可触摸区域
</div>
- 指令名 :
v-finger - 修饰符 :冒号后面是事件类型,如
tap、swipe、long-tap、pinch、rotate等。 - 值 :当前 Vue 实例上的方法名,与普通
@click一样写在 methods 里即可。
4.2 支持的事件
| 事件名 | 说明 |
|---|---|
tap |
单击 |
double-tap |
双击 |
single-tap |
单击(与 double-tap 区分时用) |
long-tap |
长按 |
swipe |
滑动手势(可结合 evt.direction) |
pinch |
双指缩放(evt.zoom) |
rotate |
双指旋转(evt.angle) |
press-move |
按住拖动(evt.deltaX / deltaY) |
multipoint-start |
多指开始 |
multipoint-end |
多指结束 |
touch-start / touch-move / touch-end / touch-cancel |
原生触摸事件封装 |
需要传参 时,在方法里接收事件对象即可(如 swipe(evt) 中的 evt.direction、pinch(evt) 中的 evt.zoom)。
4.3 完整示例
模板:
html
<template>
<div
class="touch-area"
v-finger:tap="tap"
v-finger:long-tap="longTap"
v-finger:swipe="swipe"
v-finger:pinch="pinch"
v-finger:rotate="rotate"
v-finger:double-tap="doubleTap"
v-finger:single-tap="singleTap"
>
<div>点我、长按、滑动或双指操作</div>
</div>
</template>
脚本:
javascript
export default {
methods: {
tap() {
console.log('单击')
},
longTap() {
console.log('长按')
},
swipe(evt) {
console.log('滑动方向:', evt.direction)
},
pinch(evt) {
console.log('缩放比例:', evt.zoom)
},
rotate(evt) {
console.log('旋转角度:', evt.angle)
},
doubleTap() {
console.log('双击')
},
singleTap() {
console.log('单击(与双击区分)')
}
}
}
按需绑定自己用到的几个事件即可,不必全部写上。
五、用法很简单,那AlloyFinger是怎么实现的呢?
了解实现原理,有助于我们更放心地使用、排查问题,甚至做简单扩展。
AlloyFinger 的实现可以拆成两层:底层手势识别(alloy_finger.js) 和 Vue 指令封装(alloy_finger_vue.js)。
5.1 底层:基于原生 Touch 事件 + 向量运算
AlloyFinger 不依赖任何框架,核心就是给一个 DOM 元素绑定四个原生事件:
javascript
this.element.addEventListener("touchstart", this.start, false);
this.element.addEventListener("touchmove", this.move, false);
this.element.addEventListener("touchend", this.end, false);
this.element.addEventListener("touchcancel", this.cancel, false);
在 start 里:
- 记录第一个触点的坐标
(x1, y1)和当前时间戳; - 用「上次 tap 的时间」和「两次点击的位移」判断是否构成双击(例如 250ms 内、位移 30px 以内);
- 若检测到多指(
evt.touches.length > 1),则计算两指构成的向量长度,作为后续 pinch 缩放的基准 ,并触发multipointStart; - 同时启动一个 750ms 的定时器 ,到时即触发 longTap。
在 move 里:
- 若是单指 ,则用当前点与上一帧点的差值得到
deltaX、deltaY,触发 pressMove; - 若移动距离超过约 10px,会置位
_preventTap,避免误触 tap。 - 若是双指 ,则用两指构成的向量做向量长度比 得到
evt.zoom(pinch),用向量夹角 得到evt.angle(rotate),这里用到简单的向量数学(点积、叉积、夹角),核心逻辑类似:
javascript
// 向量长度
function getLen(v) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
// 缩放:当前两指距离 / 起始两指距离
evt.zoom = getLen(v) / this.pinchStartLen;
// 旋转:当前向量相对上一帧向量的角度
evt.angle = getRotateAngle(v, preV);
在 end 里:
- 若「起点到终点的位移」超过约 30px,则根据 x、y 方向位移谁更大来判定 swipe 方向 (Left/Right/Up/Down),并触发
swipe; - 否则在下一个「事件循环」里触发 tap ,并根据之前的双击标记决定是否再触发 doubleTap 或延迟 250ms 触发 singleTap;
- 同时会清除 longTap 定时器、重置双指相关的状态。
也就是说:tap / longTap / doubleTap / swipe / pinch / rotate / pressMove 等,都是在同一套 touch 生命周期里,用「时间差 + 位移 + 向量运算」推导出来的,没有黑魔法。
5.2 回调管理:HandlerAdmin
每种手势对应一个「回调列表」,用 HandlerAdmin 统一管理:add 注册、del 移除、dispatch 时对该元素上的所有回调依次 apply。这样同一个元素上可以挂多个监听(例如 Vue 插件里对同一元素绑定多个 v-finger:xxx),彼此也不会互相覆盖。
5.3 Vue 插件层:v-finger 如何挂到 DOM 上
插件在 install 时执行 Vue.directive('finger', directiveOpts),因此模板里的 v-finger 会变成对自定义指令 finger 的调用。
- 事件名映射 :模板里写的是 kebab-case(如
v-finger:long-tap),插件里用 EVENTMAP 转成 AlloyFinger 的 camelCase(如longTap),再交给底层。 - 一元素一实例 :用一个全局 CACHE 数组,按 DOM 元素存
{ elem, alloyFinger }。同一元素上多条v-finger:tap、v-finger:swipe等,共用一个 AlloyFinger 实例 ;第一次绑定时new AlloyFinger(elem, options),之后同元素再绑其他事件时,不再 new,而是alloyFinger.on(eventName, func)往该实例上追加回调。 - 指令生命周期 :Vue2 下
bind/update时执行doBindEvent(绑定或更新回调),unbind时从 CACHE 里取出实例并调用alloyFinger.destroy(),移除原生事件监听和所有定时器,避免内存泄漏。
核心片段:
javascript
// 同一元素多次 v-finger:xxx 共用一个 AlloyFinger 实例
var cacheObj = CACHE[getElemCacheIndex(elem)];
if (cacheObj && cacheObj.alloyFinger) {
if (oldFunc) cacheObj.alloyFinger.off(eventName, oldFunc);
if (func) cacheObj.alloyFinger.on(eventName, func);
} else {
CACHE.push({
elem: elem,
alloyFinger: new AlloyFinger(elem, { [eventName]: func })
});
}
5.4 小结
- 手势识别 :完全基于
touchstart/touchmove/touchend,用时间、位移和向量运算区分 tap、doubleTap、longTap、swipe、pinch、rotate、pressMove 等。 - Vue 层 :通过自定义指令
v-finger和元素级 AlloyFinger 实例缓存,把「模板里的 v-finger:事件名」映射到「底层 AlloyFinger 的 on/off」,实现声明式绑定与组件销毁时的清理。
参考
- AlloyFinger 仓库:AlloyTeam/AlloyFinger
- 依赖:transformjs