次元图库
36.81M · 2026-03-09
在 H5 里,原生 touchstart / touchmove / touchend 只能告诉你「手指动了」,至于用户是单击、双击、长按、滑动、双指缩放还是旋转,都要自己算时间差、距离、角度——既难写又容易出 bug。
AlloyFinger 是腾讯 AlloyTeam 开源的轻量级手势库,把这些常见手势都封装好了,并且提供了 Vue 插件,以自定义指令 v-finger 的形式在模板里绑定,写法清晰、易维护。
在项目根目录执行:
npm install alloyfinger
在 Vue 入口文件(如 src/main.js)中做两件事:
Vue.use(AlloyFingerPlugin, { AlloyFinger }) 注册。这样全局就可以在任意组件的模板里使用 v-finger 指令。
// 引入 alloy-finger
import AlloyFinger from 'alloyfinger'
import AlloyFingerPlugin from 'alloyfinger/vue/alloy_finger_vue'
Vue.use(AlloyFingerPlugin, {
AlloyFinger
})
注意:
alloyfinger/vue/alloy_finger_vueVue.use 的第二个参数传进去,插件内部会用它来创建手势实例。注册完成后,在任意 Vue 组件的模板中,给需要绑定手势的单个根元素写上 v-finger:事件名="方法名" 即可。
<div
v-finger:tap="onTap"
v-finger:swipe="onSwipe"
v-finger:long-tap="onLongTap"
>
可触摸区域
</div>
v-fingertap、swipe、long-tap、pinch、rotate 等。@click 一样写在 methods 里即可。| 事件名 | 说明 |
|---|---|
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)。
模板:
<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>
脚本:
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 的实现可以拆成两层:底层手势识别(alloy_finger.js) 和 Vue 指令封装(alloy_finger_vue.js)。
AlloyFinger 不依赖任何框架,核心就是给一个 DOM 元素绑定四个原生事件:
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)和当前时间戳;evt.touches.length > 1),则计算两指构成的向量长度,作为后续 pinch 缩放的基准,并触发 multipointStart;在 move 里:
deltaX、deltaY,触发 pressMove;_preventTap,避免误触 tap。evt.zoom(pinch),用向量夹角得到 evt.angle(rotate),这里用到简单的向量数学(点积、叉积、夹角),核心逻辑类似:// 向量长度
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 里:
swipe;也就是说:tap / longTap / doubleTap / swipe / pinch / rotate / pressMove 等,都是在同一套 touch 生命周期里,用「时间差 + 位移 + 向量运算」推导出来的,没有黑魔法。
每种手势对应一个「回调列表」,用 HandlerAdmin 统一管理:add 注册、del 移除、dispatch 时对该元素上的所有回调依次 apply。这样同一个元素上可以挂多个(例如 Vue 插件里对同一元素绑定多个 v-finger:xxx),彼此也不会互相覆盖。
插件在 install 时执行 Vue.directive('finger', directiveOpts),因此模板里的 v-finger 会变成对自定义指令 finger 的调用。
v-finger:long-tap),插件里用 EVENTMAP 转成 AlloyFinger 的 camelCase(如 longTap),再交给底层。{ elem, alloyFinger }。同一元素上多条 v-finger:tap、v-finger:swipe 等,共用一个 AlloyFinger 实例;第一次绑定时 new AlloyFinger(elem, options),之后同元素再绑其他事件时,不再 new,而是 alloyFinger.on(eventName, func) 往该实例上追加回调。bind / update 时执行 doBindEvent(绑定或更新回调),unbind 时从 CACHE 里取出实例并调用 alloyFinger.destroy(),移除原生事件和所有定时器,避免内存泄漏。核心片段:
// 同一元素多次 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 })
});
}
touchstart / touchmove / touchend,用时间、位移和向量运算区分 tap、doubleTap、longTap、swipe、pinch、rotate、pressMove 等。v-finger 和元素级 AlloyFinger 实例缓存,把「模板里的 v-finger:事件名」映射到「底层 AlloyFinger 的 on/off」,实现声明式绑定与组件销毁时的清理。