zoukankan      html  css  js  c++  java
  • 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

    开心的做一个无忧无虑的码农,争取每天进步一点。
  • 相关阅读:
    CentOS 7/8修改系统运行级别
    Sketchup 汇总
    [转]Ruby之类的真相
    [转]Ruby之代码块的迷思
    [转]ruby中的异常处理
    [转]ruby中Class的allocate
    [转]ruby中require和load的区别
    [转]ruby之方法查找
    [转]ruby之动态方法
    [转] ruby之对象体系
  • 原文地址:https://www.cnblogs.com/sorrowx/p/6510525.html
Copyright © 2011-2022 走看看