此手势库利用了手机端touchstart, touchmove, touchend, touchcancel原生事件模拟出了 rotate  touchStart  multipointStart  multipointEnd  pinch  swipe  tap  doubleTap  longTap  singleTap  pressMove  touchMove  touchEnd  touchCancel这14个事件回调给用户去使用。下面会讲述几个常用的手势原理实现。

先来看一下我对源码的理解, 注意关于rotate旋转手势,我个人觉得可能理解的不对(但是我会把我的笔记放在下面),希望有人能够指出我的问题,谢谢了。

源码笔记:

 /* AlloyFinger v0.1.4
* By dntzhang
* Github: https://github.com/AlloyTeam/AlloyFinger
* Sorrow.X --- 添加注释,注释纯属个人理解(关于rotate旋转手势,理解的还不透彻)
*/
; (function () {
// 一些要使用的内部工具函数 // 2点之间的距离 (主要用来算pinch的比例的, 两点之间的距离比值求pinch的scale)
function getLen(v) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}; // dot和getAngle函数用来算两次手势状态之间的夹角, cross函数用来算方向的, getRotateAngle函数算手势真正的角度的
function dot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}; // 求两次手势状态之间的夹角
function getAngle(v1, v2) {
var mr = getLen(v1) * getLen(v2);
if (mr === 0) return 0;
var r = dot(v1, v2) / mr;
if (r > 1) r = 1;
return Math.acos(r);
}; // 利用cross结果的正负来判断旋转的方向(大于0为逆时针, 小于0为顺时针)
function cross(v1, v2) {
return v1.x * v2.y - v2.x * v1.y;
}; // 如果cross大于0那就是逆时针对于屏幕是正角,对于第一象限是负角,所以 角度 * -1, 然后角度单位换算
function getRotateAngle(v1, v2) {
var angle = getAngle(v1, v2);
if (cross(v1, v2) > 0) {
angle *= -1;
};
return angle * 180 / Math.PI;
}; // HandlerAdmin构造函数
var HandlerAdmin = function(el) {
this.handlers = []; // 手势函数集合
this.el = el; // dom元素
}; // HandlerAdmin原型方法 // 把fn添加到实例的 handlers数组中
HandlerAdmin.prototype.add = function(handler) {
this.handlers.push(handler);
}; // 删除 handlers数组中的函数
HandlerAdmin.prototype.del = function(handler) {
if(!handler) this.handlers = []; // handler为假值,handlers则赋值为[](参数不传undefined,其实不管this.handlers有没有成员函数,都得置空) for(var i = this.handlers.length; i >= 0; i--) {
if(this.handlers[i] === handler) { // 如果函数一样
this.handlers.splice(i, 1); // 从handler中移除该函数(改变了原数组)
};
};
}; // 执行用户的回调函数
HandlerAdmin.prototype.dispatch = function() {
for(var i=0, len=this.handlers.length; i<len; i++) {
var handler = this.handlers[i];
if(typeof handler === 'function') handler.apply(this.el, arguments); // 执行回调this为dom元素, 把触发的事件对象作为参数传过去了
};
}; function wrapFunc(el, handler) {
var handlerAdmin = new HandlerAdmin(el); // 实例化一个对象
handlerAdmin.add(handler); return handlerAdmin;
}; // AlloyFinger构造函数
var AlloyFinger = function (el, option) { // el: dom元素/id, option: 各种手势的集合对象 this.element = typeof el == 'string' ? document.querySelector(el) : el; // 获取dom元素 // 绑定原型上start, move, end, cancel函数的this对象为 AlloyFinger实例
this.start = this.start.bind(this);
this.move = this.move.bind(this);
this.end = this.end.bind(this);
this.cancel = this.cancel.bind(this); // 给dom元素 绑定原生的 touchstart, touchmove, touchend, touchcancel事件, 默认冒泡
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); this.preV = { x: null, y: null }; // 开始前的坐标
this.pinchStartLen = null; // start()方法开始时捏的长度
this.scale = 1; // 初始缩放比例为1
this.isDoubleTap = false; // 是否双击, 默认为false var noop = function () { }; // 空函数(把用户没有绑定手势函数默认赋值此函数) // 提供了14种手势函数. 根据option对象, 分别创建一个 HandlerAdmin实例 赋值给相应的this属性
this.rotate = wrapFunc(this.element, option.rotate || noop);
this.touchStart = wrapFunc(this.element, option.touchStart || noop);
this.multipointStart = wrapFunc(this.element, option.multipointStart || noop);
this.multipointEnd = wrapFunc(this.element, option.multipointEnd || noop);
this.pinch = wrapFunc(this.element, option.pinch || noop);
this.swipe = wrapFunc(this.element, option.swipe || noop);
this.tap = wrapFunc(this.element, option.tap || noop);
this.doubleTap = wrapFunc(this.element, option.doubleTap || noop);
this.longTap = wrapFunc(this.element, option.longTap || noop);
this.singleTap = wrapFunc(this.element, option.singleTap || noop);
this.pressMove = wrapFunc(this.element, option.pressMove || noop);
this.touchMove = wrapFunc(this.element, option.touchMove || noop);
this.touchEnd = wrapFunc(this.element, option.touchEnd || noop);
this.touchCancel = wrapFunc(this.element, option.touchCancel || noop); this.delta = null; // 差值 变量增量
this.last = null; // 最后数值
this.now = null; // 开始时的时间戳
this.tapTimeout = null; // tap超时
this.singleTapTimeout = null; // singleTap超时
this.longTapTimeout = null; // longTap超时(定时器的返回值)
this.swipeTimeout = null; // swipe超时
this.x1 = this.x2 = this.y1 = this.y2 = null; // start()时的坐标x1, y1, move()时的坐标x2, y2 (相对于页面的坐标)
this.preTapPosition = { x: null, y: null }; // 用来保存start()方法时的手指坐标
}; // AlloyFinger原型对象
AlloyFinger.prototype = { start: function (evt) {
if (!evt.touches) return; // 如果没有TouchList对象, 直接return掉 (touches: 位于屏幕上的所有手指的列表) this.now = Date.now(); // 开始时间戳
this.x1 = evt.touches[0].pageX; // 相对于页面的 x1, y1 坐标
this.y1 = evt.touches[0].pageY;
this.delta = this.now - (this.last || this.now); // 时间戳差值 this.touchStart.dispatch(evt); // 调用HandlerAdmin实例this.touchStart上的dispatch方法(用户的touchStart回调就在此调用的) if (this.preTapPosition.x !== null) { // 开始前tap的x坐标不为空的话(一次没点的时候必然是null了)
this.isDoubleTap = (this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30);
};
this.preTapPosition.x = this.x1; // 把相对于页面的 x1, y1 坐标赋值给 this.preTapPosition
this.preTapPosition.y = this.y1;
this.last = this.now; // 把开始时间戳赋给 this.last
var preV = this.preV, // 把开始前的坐标赋给 preV变量
len = evt.touches.length; // len: 手指的个数 if (len > 1) { // 一根手指以上
this._cancelLongTap(); // 取消长按定时器
this._cancelSingleTap(); // 取消SingleTap定时器 var v = { // 2个手指坐标的差值
x: evt.touches[1].pageX - this.x1,
y: evt.touches[1].pageY - this.y1
};
preV.x = v.x; // 差值赋值给PreV对象
preV.y = v.y; this.pinchStartLen = getLen(preV); // start()方法中2点之间的距离
this.multipointStart.dispatch(evt); // (用户的multipointStart回调就在此调用的)
}; this.longTapTimeout = setTimeout(function () {
this.longTap.dispatch(evt); // (用户的longTap回调就在此调用的)
}.bind(this), 750);
}, move: function (evt) {
if (!evt.touches) return; // 如果没有TouchList对象, 直接return掉 (touches: 位于屏幕上的所有手指的列表) var preV = this.preV, // 把start方法保存的2根手指坐标的差值xy赋给preV变量
len = evt.touches.length, // 手指个数
currentX = evt.touches[0].pageX, // 第一根手指的坐标(相对于页面的 x1, y1 坐标)
currentY = evt.touches[0].pageY;
console.log(preV);
this.isDoubleTap = false; // 移动过程中不能双击了 if (len > 1) { // 2根手指以上(处理捏pinch和旋转rotate手势) var v = { // 第二根手指和第一根手指坐标的差值
x: evt.touches[1].pageX - currentX,
y: evt.touches[1].pageY - currentY
}; if (preV.x !== null) { // start方法中保存的this.preV的x不为空的话 if (this.pinchStartLen > 0) { // 2点间的距离大于0
evt.scale = getLen(v) / this.pinchStartLen; // move中的2点之间的距离除以start中的2点的距离就是缩放比值
this.pinch.dispatch(evt); // scale挂在到evt对象上 (用户的pinch回调就在此调用的)
}; evt.angle = getRotateAngle(v, preV); // 计算angle角度
this.rotate.dispatch(evt); // (用户的pinch回调就在此调用的)
}; preV.x = v.x; // 把move中的2根手指中的差值赋值给preV, 当然也改变了this.preV
preV.y = v.y; } else { // 一根手指(处理拖动pressMove手势) if (this.x2 !== null) { // 一根手指第一次必然为空,因为初始化赋值为null, 下面将会用x2, y2保存上一次的结果 evt.deltaX = currentX - this.x2; // 拖动过程中或者手指移动过程中的差值(当前坐标与上一次的坐标)
evt.deltaY = currentY - this.y2; } else {
evt.deltaX = 0; // 第一次嘛, 手指刚移动,哪来的差值啊,所以为0呗
evt.deltaY = 0;
};
this.pressMove.dispatch(evt); // deltaXY挂载到evt对象上,抛给用户(用户的pressMove回调就在此调用的)
}; this.touchMove.dispatch(evt); // evt对象因if语句而不同,挂载不同的属性抛出去给用户 (用户的touchMove回调就在此调用的) this._cancelLongTap(); // 取消长按定时器 this.x2 = currentX; // 存一下本次的pageXY坐标, 为了下次做差值
this.y2 = currentY; if (len > 1) { // 2个手指以上就阻止默认事件
evt.preventDefault();
};
}, end: function (evt) {
if (!evt.changedTouches) return; // 位于该元素上的所有手指的列表, 没有TouchList也直接return掉 this._cancelLongTap(); // 取消长按定时器 var self = this; // 存个实例
if (evt.touches.length < 2) { // 手指数量小于2就触发 (用户的多点结束multipointEnd回调函数)
this.multipointEnd.dispatch(evt);
}; this.touchEnd.dispatch(evt); // 触发(用户的touchEnd回调函数) //swipe 滑动
if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) || (this.y2 && Math.abs(this.preV.y - this.y2) > 30)) { evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2); // 获取上下左右方向中的一个 this.swipeTimeout = setTimeout(function () {
self.swipe.dispatch(evt); // 立即触发,加入异步队列(用户的swipe事件的回调函数)
}, 0); } else { // 以下是tap, singleTap, doubleTap事件派遣 this.tapTimeout = setTimeout(function () { self.tap.dispatch(evt); // 触发(用户的tap事件的回调函数)
// trigger double tap immediately
if (self.isDoubleTap) { // 如果满足双击的话 self.doubleTap.dispatch(evt); // 触发(用户的doubleTap事件的回调函数)
clearTimeout(self.singleTapTimeout); // 清除singleTapTimeout定时器 self.isDoubleTap = false; // 双击条件重置 } else {
self.singleTapTimeout = setTimeout(function () {
self.singleTap.dispatch(evt); // 触发(用户的singleTap事件的回调函数)
}, 250);
}; }, 0); // 加入异步队列,主线程完成立马执行
}; this.preV.x = 0; // this.preV, this.scale, this.pinchStartLen, this.x1 x2 y1 y2恢复初始值
this.preV.y = 0;
this.scale = 1;
this.pinchStartLen = null;
this.x1 = this.x2 = this.y1 = this.y2 = null;
}, cancel: function (evt) {
//清除定时器
clearTimeout(this.singleTapTimeout);
clearTimeout(this.tapTimeout);
clearTimeout(this.longTapTimeout);
clearTimeout(this.swipeTimeout);
// 执行用户的touchCancel回调函数,没有就执行一次noop空函数
this.touchCancel.dispatch(evt);
}, _cancelLongTap: function () { // 取消长按定时器
clearTimeout(this.longTapTimeout);
}, _cancelSingleTap: function () { // 取消延时SingleTap定时器
clearTimeout(this.singleTapTimeout);
}, // 2点间x与y之间的绝对值的差值作比较,x大的话即为左右滑动,y大即为上下滑动,x的差值大于0即为左滑动,小于0即为右滑动
_swipeDirection: function (x1, x2, y1, y2) { // 判断用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动
return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down');
}, // 给dom添加14种事件中的一种
on: function(evt, handler) {
if(this[evt]) { // 看看有没有相应的事件名
this[evt].add(handler); // HandlerAdmin实例的add方法
};
}, // 移除手势事件对应函数
off: function(evt, handler) {
if(this[evt]) {
this[evt].del(handler); // 从数组中删除handler方法
};
}, destroy: function() { // 关闭所有定时器
if(this.singleTapTimeout) clearTimeout(this.singleTapTimeout);
if(this.tapTimeout) clearTimeout(this.tapTimeout);
if(this.longTapTimeout) clearTimeout(this.longTapTimeout);
if(this.swipeTimeout) clearTimeout(this.swipeTimeout); // 取消dom上touchstart, touchmove, touchend, touchcancel事件
this.element.removeEventListener("touchstart", this.start);
this.element.removeEventListener("touchmove", this.move);
this.element.removeEventListener("touchend", this.end);
this.element.removeEventListener("touchcancel", this.cancel); // 把14个HandlerAdmin实例的this.handlers置为空数组
this.rotate.del();
this.touchStart.del();
this.multipointStart.del();
this.multipointEnd.del();
this.pinch.del();
this.swipe.del();
this.tap.del();
this.doubleTap.del();
this.longTap.del();
this.singleTap.del();
this.pressMove.del();
this.touchMove.del();
this.touchEnd.del();
this.touchCancel.del(); // 实例成员的变量全部置为null
this.preV = this.pinchStartLen = this.scale = this.isDoubleTap = this.delta = this.last = this.now = this.tapTimeout = this.singleTapTimeout = this.longTapTimeout = this.swipeTimeout = this.x1 = this.x2 = this.y1 = this.y2 = this.preTapPosition = this.rotate = this.touchStart = this.multipointStart = this.multipointEnd = this.pinch = this.swipe = this.tap = this.doubleTap = this.longTap = this.singleTap = this.pressMove = this.touchMove = this.touchEnd = this.touchCancel = null; return null;
}
}; // 抛出去
if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = AlloyFinger;
} else {
window.AlloyFinger = AlloyFinger;
};
})();

使用姿势:

             var af = new AlloyFinger(testDiv, {
touchStart: function () {
html = "";
html += "start0<br/>";
result.innerHTML = html; },
touchEnd: function () {
html += "end<br/>";
result.innerHTML = html; },
tap: function () {
html += "tap<br/>";
result.innerHTML = html;
},
singleTap: function() {
html += "singleTap<br/>";
result.innerHTML = html;
},
longTap: function() {
html += "longTap<br/>";
result.innerHTML = html;
},
rotate: function (evt) {
html += "rotate [" + evt.angle + "]<br/>";
result.innerHTML = html;
},
pinch: function (evt) {
html += "pinch [" + evt.scale + "]<br/>";
result.innerHTML = html;
},
pressMove: function (evt) {
html += "pressMove [" + evt.deltaX.toFixed(4) + "|" + evt.deltaY.toFixed(4) + "]<br/>";
result.innerHTML = html;
evt.preventDefault();
},
touchMove: function (evt) {
html += "touchMove [" + evt.deltaX.toFixed(4) + "|" + evt.deltaY.toFixed(4) + "]<br/>";
result.innerHTML = html;
evt.preventDefault();
},
swipe: function (evt) {
html += "swipe [" + evt.direction+"]<br/>";
result.innerHTML = html;
}
}); af.on('touchStart', touchStart1);
af.on('touchStart', touchStart2); // 多次添加只会把方法添加到HandlerAdmin实例的handlers数组中,会依次遍历执行添加的函数 function touchStart1() {
html += "start1<br/>";
result.innerHTML = html;
}; function touchStart2() {
html += "start2<br/>";
result.innerHTML = html;
}; af.off('touchStart', touchStart2); af.on('longTap', function(evt) {
evt.preventDefault();
af.destroy();
html += "已销毁所有事件!<br/>";
result.innerHTML = html;
});

下面会讲述几个很常用的手势原理:

tap点按:

移动端click有300毫秒延时,tap的本质其实就是touchend。

但是要(244行)判断touchstart的手的坐标和touchend时候手的坐标x、y方向偏移要小于30。小于30才会去触发tap。

longTap长按:

touchstart开启一个750毫秒的settimeout,如果750ms内有touchmove或者touchend都会清除掉该定时器。

超过750ms没有touchmove或者touchend就会触发longTap

swipe划动:

当touchstart的手的坐标和touchend时候手的坐标x、y方向偏移要大于30,判断swipe,小于30会判断tap。

那么用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动呢?

2点间x与y之间的绝对值的差值作比较,x大的话即为左右滑动,y大即为上下滑动,x的差值大于0即为左滑动,小于0即为右滑动,

y的差值大于0为上,小于0为下.

pinch捏:

    这个就是start()时2个手指间的距离和move()时2个手指的距离的比值就是scale。这个scale会挂载在event上抛给用户。

rotate旋转:

    这个还真没怎么弄明白,先看一下原作者的原理解释:

如上图所示,利用内积,可以求出两次手势状态之间的夹角θ。但是这里怎么求旋转方向呢?那么就要使用差乘(Vector Cross)。
利用cross结果的正负来判断旋转的方向。

cross本质其实是面积,可以看下面的推导:

所以,物理引擎里经常用cross来计算转动惯量,因为力矩其实要是力乘矩相当于面积:

反正我没怎么理解最后一张图了。他的推导公式,我是这么化简的,如下:

我的c向量使用的是(y2, -x2),其实还有一个是(-y2, x2)。如果使用(-y2, x2)这个求出来的面试公式就和上面的公式就差了一个负号了。在getRotateAngle函数中,判断条件也要相应的改成

if (cross(v1, v2) < 0) {
    angle *= -1;
};

这样才行了,好吧暂时先这么理解rotate旋转的公式吧。

ps: 很不错的一个手机端的手势库,代码简洁,功能强悍。

github地址: https://github.com/AlloyTeam/AlloyFinger

AlloyFinger.js 源码 学习笔记及原理说明的更多相关文章

  1. AlloyTouch.js 源码 学习笔记及原理说明

    alloyTouch这个库其实可以做很多事的, 比较抽象, 需要我们用户好好的思考作者提供的实例属性和一些回调方法(touchStart, change, touchMove, pressMove, ...

  2. lazy-load-img.js 源码 学习笔记及原理说明

    lazy-load-img.js? 1. 什么鬼? 一个轻量级的图片懒加载,我个人很是喜欢. 2. 有什么优势? 1.原生js开发,不依赖任何框架或库 2.支持将各种宽高不一致的图片,自动剪切成默认图 ...

  3. Underscore.js 源码学习笔记(下)

    上接 Underscore.js 源码学习笔记(上) === 756 行开始 函数部分. var executeBound = function(sourceFunc, boundFunc, cont ...

  4. Underscore.js 源码学习笔记(上)

    版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}());  这样的东西,我们应该知道这是一个 IIFE(立即执行 ...

  5. Vue.js 源码学习笔记

    最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...

  6. move.js 源码 学习笔记

    源码笔记: /* move.js * @author:flfwzgl https://github.com/flfwzgl * @copyright: MIT license * Sorrow.X - ...

  7. observe.js 源码 学习笔记

    /** * observejs --- By dnt http://kmdjs.github.io/ * Github: https://github.com/kmdjs/observejs * MI ...

  8. Vue.js 源码学习笔记 -- 分析前准备1 -- vue三大利器

    主体 实例方法归类:   先看个作者推荐, 清晰易懂的  23232 简易编译器   重点: 最简单的订阅者模式 // Observer class Observer { constructor (d ...

  9. Vue.js 源码学习笔记 - 细节

     1. this._eventsCount = { }    这是为了避免不必要的深度遍历: 在有广播事件到来时,如果当前 vm 的 _eventsCount 为 0, 则不必向其子 vm 继续传播该 ...

随机推荐

  1. WPF中将16进制颜色码转换成SolidColorBrush

    使用ColorConverter.ConvertFromString(string colorValue)方法 例如:new SolidColorBrush((Color)ColorConverter ...

  2. 排名前10的H5、Js 3D游戏引擎和框架

    由于很多人都在用JavaScript.HTML5和WebGL技术创建基于浏览器的3D游戏,所有JavaScript 3D游戏引擎是一个人们主题.基于浏览器的游戏最棒的地方是平台独立,它们能在iOS.A ...

  3. Spring mvc 数据验证

    加入jar包 bean-validator.jar 在实体类中加入验证Annotation和消息提示 package com.stone.model; import javax.validation. ...

  4. Javaweb程序服务器部署

    话说从接触web后就想着写一个自己的站点然后别人都可以访问,这也是一个小目标吧,从之前在使用校园网的时候把自己的电脑当成服务器然后部署使用同学的电脑访问,现在想让所有人都可以访问,于是就花重金租了腾讯 ...

  5. (五)Hololens Unity 开发之 手势识别

    学习源于官方文档 Gestures in Unity 笔记一部分是直接翻译官方文档,部分各人理解不一致的和一些比较浅显的保留英文原文 (五)Hololens Unity 开发之 手势识别 HoloLe ...

  6. TimSort算法分析

    Timsort是一种混合稳定的排序算法,采用归并排序混合插入排序的设计,在多种真实数据上表现良好. 它基于一个简单的事实,实际中大部分数据都是部分有序(升序或降序)的. 它于2002年由Tim Pet ...

  7. 笑谈ArcToolbox (2) 开启ArcToolbox的钥匙

    笑谈ArcToolbox (2) 开启ArcToolbox的钥匙 by 李远祥 GIS人遇到ArcToolbox就像找到了宝藏一样兴奋,但并不是每个找到宝藏的人都具备开启宝藏的钥匙.有时候功能强大并不 ...

  8. datagridview数据绑定操作数据库实现增删改查

    DataSet数据集,数据缓存在客户端内存中,支持断开式连接.DataGridView控件绑定DataSet时,它自动的改变的DS的行的状态,而且在做增删改查的时候,可以借助SqlCommandBui ...

  9. Linux编程之ICMP洪水攻击

    我的上一篇文章<Linux编程之PING的实现>里使用ICMP协议实现了PING的程序,ICMP除了实现这么一个PING程序,还有哪些不为人知或者好玩的用途?这里我将介绍ICMP另一个很有 ...

  10. Android jni 编程2(对基本类型一维整型数组的操作)

    参考教程和这位博主的对一维数组的处理,主要包括以下三种类型: //传入一维数组,无返回值 public native void arrayEncode(int[] arr); //传一个一维数组和数组 ...