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

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

 

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

 

源码笔记:

 

  1 /* AlloyFinger v0.1.4
  2  * By dntzhang
  3  * Github: https://github.com/AlloyTeam/AlloyFinger
  4  * Sorrow.X --- 添加注释,注释纯属个人理解(关于rotate旋转手势,理解的还不透彻)
  5  */
  6 ; (function () {
  7     // 一些要使用的内部工具函数
  8 
  9     // 2点之间的距离 (主要用来算pinch的比例的, 两点之间的距离比值求pinch的scale)
 10     function getLen(v) {
 11         return Math.sqrt(v.x * v.x + v.y * v.y);
 12     };
 13 
 14     // dot和getAngle函数用来算两次手势状态之间的夹角, cross函数用来算方向的, getRotateAngle函数算手势真正的角度的
 15     function dot(v1, v2) {
 16         return v1.x * v2.x + v1.y * v2.y;
 17     };
 18 
 19     // 求两次手势状态之间的夹角
 20     function getAngle(v1, v2) {
 21         var mr = getLen(v1) * getLen(v2);
 22         if (mr === 0) return 0;
 23         var r = dot(v1, v2) / mr;
 24         if (r > 1) r = 1;
 25         return Math.acos(r);
 26     };
 27 
 28     // 利用cross结果的正负来判断旋转的方向(大于0为逆时针, 小于0为顺时针)
 29     function cross(v1, v2) {
 30         return v1.x * v2.y - v2.x * v1.y;
 31     };
 32 
 33     // 如果cross大于0那就是逆时针对于屏幕是正角,对于第一象限是负角,所以 角度 * -1, 然后角度单位换算
 34     function getRotateAngle(v1, v2) {
 35         var angle = getAngle(v1, v2);
 36         if (cross(v1, v2) > 0) {
 37             angle *= -1;
 38         };
 39         return angle * 180 / Math.PI;
 40     };
 41 
 42     // HandlerAdmin构造函数
 43     var HandlerAdmin = function(el) {
 44         this.handlers = [];    // 手势函数集合
 45         this.el = el;    // dom元素
 46     };
 47 
 48     // HandlerAdmin原型方法
 49 
 50     // 把fn添加到实例的 handlers数组中
 51     HandlerAdmin.prototype.add = function(handler) {
 52         this.handlers.push(handler); 
 53     };
 54 
 55     // 删除 handlers数组中的函数
 56     HandlerAdmin.prototype.del = function(handler) {
 57         if(!handler) this.handlers = [];    // handler为假值,handlers则赋值为[](参数不传undefined,其实不管this.handlers有没有成员函数,都得置空)
 58 
 59         for(var i = this.handlers.length; i >= 0; i--) {
 60             if(this.handlers[i] === handler) {    // 如果函数一样
 61                 this.handlers.splice(i, 1);    // 从handler中移除该函数(改变了原数组)
 62             };
 63         };
 64     };
 65 
 66     // 执行用户的回调函数
 67     HandlerAdmin.prototype.dispatch = function() {
 68         for(var i=0, len=this.handlers.length; i<len; i++) {
 69             var handler = this.handlers[i];    
 70             if(typeof handler === 'function') handler.apply(this.el, arguments);    // 执行回调this为dom元素, 把触发的事件对象作为参数传过去了
 71         };
 72     };
 73 
 74     function wrapFunc(el, handler) {
 75         var handlerAdmin = new HandlerAdmin(el);    // 实例化一个对象
 76         handlerAdmin.add(handler);
 77 
 78         return handlerAdmin;
 79     };
 80 
 81     // AlloyFinger构造函数
 82     var AlloyFinger = function (el, option) {    // el: dom元素/id, option: 各种手势的集合对象
 83 
 84         this.element = typeof el == 'string' ? document.querySelector(el) : el;    // 获取dom元素
 85 
 86         // 绑定原型上start, move, end, cancel函数的this对象为 AlloyFinger实例
 87         this.start = this.start.bind(this);
 88         this.move = this.move.bind(this);
 89         this.end = this.end.bind(this);
 90         this.cancel = this.cancel.bind(this);
 91 
 92         // 给dom元素 绑定原生的 touchstart, touchmove, touchend, touchcancel事件, 默认冒泡
 93         this.element.addEventListener("touchstart", this.start, false);
 94         this.element.addEventListener("touchmove", this.move, false);
 95         this.element.addEventListener("touchend", this.end, false);
 96         this.element.addEventListener("touchcancel", this.cancel, false);
 97 
 98         this.preV = { x: null, y: null };    // 开始前的坐标
 99         this.pinchStartLen = null;    // start()方法开始时捏的长度
100         this.scale = 1;    // 初始缩放比例为1
101         this.isDoubleTap = false;    // 是否双击, 默认为false
102 
103         var noop = function () { };    // 空函数(把用户没有绑定手势函数默认赋值此函数)
104 
105         // 提供了14种手势函数. 根据option对象, 分别创建一个 HandlerAdmin实例 赋值给相应的this属性
106         this.rotate = wrapFunc(this.element, option.rotate || noop);
107         this.touchStart = wrapFunc(this.element, option.touchStart || noop);
108         this.multipointStart = wrapFunc(this.element, option.multipointStart || noop);
109         this.multipointEnd = wrapFunc(this.element, option.multipointEnd || noop);
110         this.pinch = wrapFunc(this.element, option.pinch || noop);
111         this.swipe = wrapFunc(this.element, option.swipe || noop);
112         this.tap = wrapFunc(this.element, option.tap || noop);
113         this.doubleTap = wrapFunc(this.element, option.doubleTap || noop);
114         this.longTap = wrapFunc(this.element, option.longTap || noop);
115         this.singleTap = wrapFunc(this.element, option.singleTap || noop);
116         this.pressMove = wrapFunc(this.element, option.pressMove || noop);
117         this.touchMove = wrapFunc(this.element, option.touchMove || noop);
118         this.touchEnd = wrapFunc(this.element, option.touchEnd || noop);
119         this.touchCancel = wrapFunc(this.element, option.touchCancel || noop);
120 
121         this.delta = null;    // 差值 变量增量
122         this.last = null;    // 最后数值
123         this.now = null;    // 开始时的时间戳
124         this.tapTimeout = null;    // tap超时
125         this.singleTapTimeout = null;    // singleTap超时
126         this.longTapTimeout = null;    // longTap超时(定时器的返回值)
127         this.swipeTimeout = null;    // swipe超时
128         this.x1 = this.x2 = this.y1 = this.y2 = null;    // start()时的坐标x1, y1, move()时的坐标x2, y2 (相对于页面的坐标)
129         this.preTapPosition = { x: null, y: null };    // 用来保存start()方法时的手指坐标
130     };
131 
132     // AlloyFinger原型对象
133     AlloyFinger.prototype = {
134 
135         start: function (evt) {
136             if (!evt.touches) return;    // 如果没有TouchList对象, 直接return掉 (touches: 位于屏幕上的所有手指的列表)
137 
138             this.now = Date.now();    // 开始时间戳
139             this.x1 = evt.touches[0].pageX;    // 相对于页面的 x1, y1 坐标
140             this.y1 = evt.touches[0].pageY;
141             this.delta = this.now - (this.last || this.now);    // 时间戳差值
142 
143             this.touchStart.dispatch(evt);    // 调用HandlerAdmin实例this.touchStart上的dispatch方法(用户的touchStart回调就在此调用的)
144 
145             if (this.preTapPosition.x !== null) {    // 开始前tap的x坐标不为空的话(一次没点的时候必然是null了)
146                 this.isDoubleTap = (this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30);
147             };
148             this.preTapPosition.x = this.x1;    // 把相对于页面的 x1, y1 坐标赋值给 this.preTapPosition
149             this.preTapPosition.y = this.y1;
150             this.last = this.now;    // 把开始时间戳赋给 this.last
151             var preV = this.preV,    // 把开始前的坐标赋给 preV变量
152                 len = evt.touches.length;    // len: 手指的个数
153 
154             if (len > 1) {    // 一根手指以上
155                 this._cancelLongTap();    // 取消长按定时器
156                 this._cancelSingleTap();    // 取消SingleTap定时器
157 
158                 var v = {    // 2个手指坐标的差值
159                     x: evt.touches[1].pageX - this.x1, 
160                     y: evt.touches[1].pageY - this.y1 
161                 };
162                 preV.x = v.x;    // 差值赋值给PreV对象
163                 preV.y = v.y;
164 
165                 this.pinchStartLen = getLen(preV);    // start()方法中2点之间的距离
166                 this.multipointStart.dispatch(evt);    // (用户的multipointStart回调就在此调用的)
167             };
168 
169             this.longTapTimeout = setTimeout(function () {
170                 this.longTap.dispatch(evt);    // (用户的longTap回调就在此调用的)
171             }.bind(this), 750);
172         },
173 
174         move: function (evt) {
175             if (!evt.touches) return;    // 如果没有TouchList对象, 直接return掉 (touches: 位于屏幕上的所有手指的列表)
176 
177             var preV = this.preV,    // 把start方法保存的2根手指坐标的差值xy赋给preV变量
178                 len = evt.touches.length,    // 手指个数
179                 currentX = evt.touches[0].pageX,    // 第一根手指的坐标(相对于页面的 x1, y1 坐标)
180                 currentY = evt.touches[0].pageY;
181                 console.log(preV);
182             this.isDoubleTap = false;    // 移动过程中不能双击了
183 
184             if (len > 1) {    // 2根手指以上(处理捏pinch和旋转rotate手势)
185 
186                 var v = {    // 第二根手指和第一根手指坐标的差值
187                     x: evt.touches[1].pageX - currentX, 
188                     y: evt.touches[1].pageY - currentY 
189                 };
190 
191                 if (preV.x !== null) {    // start方法中保存的this.preV的x不为空的话
192 
193                     if (this.pinchStartLen > 0) {    // 2点间的距离大于0
194                         evt.scale = getLen(v) / this.pinchStartLen;    // move中的2点之间的距离除以start中的2点的距离就是缩放比值
195                         this.pinch.dispatch(evt);    // scale挂在到evt对象上 (用户的pinch回调就在此调用的)
196                     };
197 
198                     evt.angle = getRotateAngle(v, preV);    // 计算angle角度
199                     this.rotate.dispatch(evt);    // (用户的pinch回调就在此调用的)
200                 };
201 
202                 preV.x = v.x;    // 把move中的2根手指中的差值赋值给preV, 当然也改变了this.preV
203                 preV.y = v.y;
204 
205             } else {    // 一根手指(处理拖动pressMove手势)
206 
207                 if (this.x2 !== null) {    // 一根手指第一次必然为空,因为初始化赋值为null, 下面将会用x2, y2保存上一次的结果
208 
209                     evt.deltaX = currentX - this.x2;    // 拖动过程中或者手指移动过程中的差值(当前坐标与上一次的坐标)
210                     evt.deltaY = currentY - this.y2;
211 
212                 } else {
213                     evt.deltaX = 0;    // 第一次嘛, 手指刚移动,哪来的差值啊,所以为0呗
214                     evt.deltaY = 0;
215                 };
216                 this.pressMove.dispatch(evt);    // deltaXY挂载到evt对象上,抛给用户(用户的pressMove回调就在此调用的)
217             };
218 
219             this.touchMove.dispatch(evt);    // evt对象因if语句而不同,挂载不同的属性抛出去给用户 (用户的touchMove回调就在此调用的)
220 
221             this._cancelLongTap();    // 取消长按定时器
222 
223             this.x2 = currentX;    // 存一下本次的pageXY坐标, 为了下次做差值
224             this.y2 = currentY;
225 
226             if (len > 1) {    // 2个手指以上就阻止默认事件
227                 evt.preventDefault();
228             };
229         },
230 
231         end: function (evt) {
232             if (!evt.changedTouches) return;    // 位于该元素上的所有手指的列表, 没有TouchList也直接return掉
233 
234             this._cancelLongTap();    // 取消长按定时器
235 
236             var self = this;    // 存个实例
237             if (evt.touches.length < 2) {    // 手指数量小于2就触发 (用户的多点结束multipointEnd回调函数)
238                 this.multipointEnd.dispatch(evt);
239             };
240 
241             this.touchEnd.dispatch(evt);    // 触发(用户的touchEnd回调函数)
242 
243             //swipe 滑动
244             if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) || (this.y2 && Math.abs(this.preV.y - this.y2) > 30)) {
245 
246                 evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2);    // 获取上下左右方向中的一个
247 
248                 this.swipeTimeout = setTimeout(function () {
249                     self.swipe.dispatch(evt);    // 立即触发,加入异步队列(用户的swipe事件的回调函数)
250                 }, 0);
251 
252             } else {   // 以下是tap, singleTap, doubleTap事件派遣
253 
254                 this.tapTimeout = setTimeout(function () {
255 
256                     self.tap.dispatch(evt);    // 触发(用户的tap事件的回调函数)
257                     // trigger double tap immediately
258                     if (self.isDoubleTap) {    // 如果满足双击的话
259 
260                         self.doubleTap.dispatch(evt);    // 触发(用户的doubleTap事件的回调函数)
261                         clearTimeout(self.singleTapTimeout);    // 清除singleTapTimeout定时器
262 
263                         self.isDoubleTap = false;    // 双击条件重置
264 
265                     } else {
266                         self.singleTapTimeout = setTimeout(function () {
267                             self.singleTap.dispatch(evt);    // 触发(用户的singleTap事件的回调函数)
268                         }, 250);
269                     };
270 
271                 }, 0);    // 加入异步队列,主线程完成立马执行
272             };
273 
274             this.preV.x = 0;    // this.preV, this.scale, this.pinchStartLen, this.x1 x2 y1 y2恢复初始值
275             this.preV.y = 0;
276             this.scale = 1;
277             this.pinchStartLen = null;
278             this.x1 = this.x2 = this.y1 = this.y2 = null;
279         },
280 
281         cancel: function (evt) {
282             //清除定时器
283             clearTimeout(this.singleTapTimeout);
284             clearTimeout(this.tapTimeout);
285             clearTimeout(this.longTapTimeout);
286             clearTimeout(this.swipeTimeout);
287             // 执行用户的touchCancel回调函数,没有就执行一次noop空函数
288             this.touchCancel.dispatch(evt);
289         },
290 
291         _cancelLongTap: function () {    // 取消长按定时器
292             clearTimeout(this.longTapTimeout);
293         },
294 
295         _cancelSingleTap: function () {    // 取消延时SingleTap定时器
296             clearTimeout(this.singleTapTimeout);
297         },
298 
299         // 2点间x与y之间的绝对值的差值作比较,x大的话即为左右滑动,y大即为上下滑动,x的差值大于0即为左滑动,小于0即为右滑动
300         _swipeDirection: function (x1, x2, y1, y2) {    // 判断用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动
301             return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down');
302         },
303 
304         // 给dom添加14种事件中的一种
305         on: function(evt, handler) {    
306             if(this[evt]) {    // 看看有没有相应的事件名
307                 this[evt].add(handler);    // HandlerAdmin实例的add方法
308             };
309         },
310 
311         // 移除手势事件对应函数
312         off: function(evt, handler) {
313             if(this[evt]) {
314                 this[evt].del(handler);    // 从数组中删除handler方法
315             };
316         },
317 
318         destroy: function() {
319 
320             // 关闭所有定时器
321             if(this.singleTapTimeout) clearTimeout(this.singleTapTimeout);
322             if(this.tapTimeout) clearTimeout(this.tapTimeout);
323             if(this.longTapTimeout) clearTimeout(this.longTapTimeout);
324             if(this.swipeTimeout) clearTimeout(this.swipeTimeout);
325 
326             // 取消dom上touchstart, touchmove, touchend, touchcancel事件
327             this.element.removeEventListener("touchstart", this.start);
328             this.element.removeEventListener("touchmove", this.move);
329             this.element.removeEventListener("touchend", this.end);
330             this.element.removeEventListener("touchcancel", this.cancel);
331 
332             // 把14个HandlerAdmin实例的this.handlers置为空数组
333             this.rotate.del();
334             this.touchStart.del();
335             this.multipointStart.del();
336             this.multipointEnd.del();
337             this.pinch.del();
338             this.swipe.del();
339             this.tap.del();
340             this.doubleTap.del();
341             this.longTap.del();
342             this.singleTap.del();
343             this.pressMove.del();
344             this.touchMove.del();
345             this.touchEnd.del();
346             this.touchCancel.del();
347 
348             // 实例成员的变量全部置为null
349             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;
350 
351             return null;
352         }
353     };
354 
355     // 抛出去
356     if (typeof module !== 'undefined' && typeof exports === 'object') {
357         module.exports = AlloyFinger;
358     } else {
359         window.AlloyFinger = AlloyFinger;
360     };
361 })();

 

 使用姿势:

 

 1             var af = new AlloyFinger(testDiv, {
 2                 touchStart: function () {
 3                     html = "";
 4                     html += "start0<br/>";
 5                     result.innerHTML = html;
 6                  
 7                 },
 8                 touchEnd: function () {
 9                     html += "end<br/>";
10                     result.innerHTML = html;
11                  
12                 },
13                 tap: function () {
14                     html += "tap<br/>";
15                     result.innerHTML = html;
16                 },
17                 singleTap: function() {
18                     html += "singleTap<br/>";
19                     result.innerHTML = html;
20                 },
21                 longTap: function() {
22                     html += "longTap<br/>";
23                     result.innerHTML = html;
24                 },
25                 rotate: function (evt) {
26                     html += "rotate [" + evt.angle + "]<br/>";
27                     result.innerHTML = html;
28                 },
29                 pinch: function (evt) {
30                     html += "pinch [" + evt.scale + "]<br/>";
31                     result.innerHTML = html;
32                 },
33                 pressMove: function (evt) {
34                     html += "pressMove [" + evt.deltaX.toFixed(4) + "|" + evt.deltaY.toFixed(4) + "]<br/>";
35                     result.innerHTML = html;
36                     evt.preventDefault();
37                 },
38                 touchMove: function (evt) {
39                     html += "touchMove [" + evt.deltaX.toFixed(4) + "|" + evt.deltaY.toFixed(4) + "]<br/>";
40                     result.innerHTML = html;
41                     evt.preventDefault();
42                 },
43                 swipe: function (evt) {
44                     html += "swipe [" + evt.direction+"]<br/>";
45                     result.innerHTML = html;
46                 }
47             });
48  
49             af.on('touchStart', touchStart1);
50             af.on('touchStart', touchStart2);    // 多次添加只会把方法添加到HandlerAdmin实例的handlers数组中,会依次遍历执行添加的函数
51 
52             function touchStart1() {
53                 html += "start1<br/>";
54                 result.innerHTML = html;
55             };
56 
57             function touchStart2() {
58                 html += "start2<br/>";
59                 result.innerHTML = html;
60             };
61 
62             af.off('touchStart', touchStart2);
63 
64             af.on('longTap', function(evt) {
65                 evt.preventDefault();
66                 af.destroy();
67                 html += "已销毁所有事件!<br/>";
68                 result.innerHTML = html;
69             });

 

 

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

 

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

 

posted @ 2017-03-06 16:24  Sorrow.X  阅读(3082)  评论(0编辑  收藏  举报