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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM