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