AlloyFinger.js 源码 学习笔记及原理说明
此手势库利用了手机端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 源码 学习笔记及原理说明的更多相关文章
- AlloyTouch.js 源码 学习笔记及原理说明
alloyTouch这个库其实可以做很多事的, 比较抽象, 需要我们用户好好的思考作者提供的实例属性和一些回调方法(touchStart, change, touchMove, pressMove, ...
- lazy-load-img.js 源码 学习笔记及原理说明
lazy-load-img.js? 1. 什么鬼? 一个轻量级的图片懒加载,我个人很是喜欢. 2. 有什么优势? 1.原生js开发,不依赖任何框架或库 2.支持将各种宽高不一致的图片,自动剪切成默认图 ...
- Underscore.js 源码学习笔记(下)
上接 Underscore.js 源码学习笔记(上) === 756 行开始 函数部分. var executeBound = function(sourceFunc, boundFunc, cont ...
- Underscore.js 源码学习笔记(上)
版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}()); 这样的东西,我们应该知道这是一个 IIFE(立即执行 ...
- Vue.js 源码学习笔记
最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...
- move.js 源码 学习笔记
源码笔记: /* move.js * @author:flfwzgl https://github.com/flfwzgl * @copyright: MIT license * Sorrow.X - ...
- observe.js 源码 学习笔记
/** * observejs --- By dnt http://kmdjs.github.io/ * Github: https://github.com/kmdjs/observejs * MI ...
- Vue.js 源码学习笔记 -- 分析前准备1 -- vue三大利器
主体 实例方法归类: 先看个作者推荐, 清晰易懂的 23232 简易编译器 重点: 最简单的订阅者模式 // Observer class Observer { constructor (d ...
- Vue.js 源码学习笔记 - 细节
1. this._eventsCount = { } 这是为了避免不必要的深度遍历: 在有广播事件到来时,如果当前 vm 的 _eventsCount 为 0, 则不必向其子 vm 继续传播该 ...
随机推荐
- 进度条(ProgressBar)的功能与用法
进度条也是UI界面中一种非常实用的组件,通常用于向用户显示某个耗时操作完成的的百分比.进度条可以动态的显示进度,因此避免长时间的执行某个耗时的操作,让用户感觉程序失去了响应,从而更好的提高用户界面的友 ...
- Word常用实用知识3
纯手打,可能有错别字,使用的版本是office Word 2013 转载请注明出处 http://www.cnblogs.com/hnnydxgjj/p/6322813.html,谢谢. 分页符分页 ...
- POJ2479(dp)
Maximum sum Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 39089 Accepted: 12221 Des ...
- (汉化改进作品)BruteXSS:Xss漏洞扫描脚本
今天给大家进行汉化改进的事一款脚本工具:BruteXSS,这款脚本能自动进行插入XSS,而且可以自定义攻击载荷. 该脚本也同时提供包含了一些绕过各种WAF(Web应用防护系统)的语句. 0×01简 ...
- JS如何实现点击页面其他地方隐藏菜单?
方法一: $("#a").on("click", function(e){ $("#menu").show(); $(documen ...
- git 打卡的第一天
因为某种原因,所以不得不重新巩固下前端的基础知识,从最基本的学习还得额外的学习新知识,倍感压力之大. 昨天初略学习下git,算是自己学习的一个新知识.简单记录下,希望四海八荒的大神看过来,有错的请指导 ...
- Ioc容器BeanPostProcessor-Spring 源码系列(3)
Ioc容器BeanPostProcessor-Spring 源码系列(3) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Io ...
- Log4net配置与使用简要说明
log4net详细配置:http://logging.apache.org/log4net/本文描述如有错误,以官网的说明为准;p 一:先来看看log4net中的几个概念: 1. log4net继承机 ...
- windows下部署免费ssl证书(letsencrypt)
随着网络的发展,网络安全也越来越重要,对于网站来说,从Http升级到https也是我们要做的首要事情.要实现https,首先我们需要申请一张SSL证书,这篇文章我主要介绍下边这几个方面: 1. SSL ...
- Javascript面对对象. 第五篇
继承 继承是面向对象中一个核心的概念.其他正统面向对象语言都会用两种方式实现继承: 一个是接口实现,一个是继承. 而ECMAScript只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成. / ...