此手勢庫利用了手機端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