众所周知,浏览器暴露了四个事件给开发者,touchstart touchmove touchend touchcancel,在这四个事件的回调函数可以拿到TouchEvent。

TouchEvent:

touches:当前位于屏幕上的所有手指动作的列表

targetTouches:位于当前 DOM 元素上的手指动作的列表

changedTouches:涉及当前事件的手指动作的列表

TouchEvent里可以拿到各个手指的坐标,那么可编程性就这么产生了。

Tap点按

一个最小手势库的实现_html

移动端click有300毫秒延时,tap的本质其实就是touchend。但是要判断touchstart的手的坐标和touchend时候手的坐标x、y方向偏移要小于30。小于30才会去触发tap。

longTap长按

一个最小手势库的实现_数组_02

touchstart开启一个750毫秒的settimeout,如果750ms内有touchmove或者touchend都会清除掉该定时器。超过750ms没有touchmove或者touchend就会触发longTap

swipe划

一个最小手势库的实现_时间戳_03

这里需要注意,当touchstart的手的坐标和touchend时候手的坐标x、y方向偏移要大于30,判断swipe,小于30会判断tap。那么用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动呢?可以根据上面三个判断得出,具体的代码如下:


1 2 3

_swipeDirection: function (x1, x2, y1, y2) { return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down') }


pinch捏

这个手势是使用频率非常高的,如图像裁剪的时候放大或者缩小图片,就需要pinch。

一个最小手势库的实现_json_04

如上图所示,两点之间的距离比值求pinch的scale。这个scale会挂载在event上,让用户反馈给dom的transform或者其他元素的scale属性。

rotate旋转

一个最小手势库的实现_json_05

如上图所示,利用内积,可以求出两次手势状态之间的夹角θ。但是这里怎么求旋转方向呢?那么就要使用差乘(Vector Cross)。

利用cross结果的正负来判断旋转的方向。

cross本质其实是面积,可以看下面的推导:

一个最小手势库的实现_绑定事件_06

所以,物理引擎里经常用cross来计算转动惯量,因为力矩其实要是力乘矩相当于面积:

一个最小手势库的实现_时间戳_07

总结

主要的一些事件触发原理已经在上面讲解,还有如multipointStart、doubleTap、singleTap、multipointEnd可以看源码,不到200行的代码应该很容易消化。trigger手势事件的同时,touchStart、touchMove、touchEnd和touchCancel同样也可以监听。



/**  * myHand.js  */  "use strict";  (function(root, factory) {     if(typeof define === "function" && define.amd) {   //AMD规范   define([], function() {    return factory(root);   });  } else {   root.myHand=root.Toucher = factory(root);   //把他挂载到window对象上  } }(window, function(root, undefined) {   if(!"ontouchstart" in window) {   return;  }   var _wrapped;   //  获取对象上的类名  function _typeOf(obj) {   return Object.prototype.toString.call(obj).toLowerCase().slice(8, -1);  }   //  获取当前时间距1970/1/1时间戳  function getTimeStr() {   return +(new Date());  }   //  获取位置信息  function getPosInfo(ev) {   var _touches = ev.touches;   if(!_touches || _touches.length === 0) {    return;   }   return {    pageX: ev.touches[0].pageX,    pageY: ev.touches[0].pageY,    clientX: ev.touches[0].clientX || 0,    clientY: ev.touches[0].clientY || 0   };  }   //  绑定事件  function bindEv(el, type, fn) {   if(el.addEventListener) {    el.addEventListener(type, fn, false);   } else {    el["on" + type] = fn;   }  }   //  解绑事件  function unBindEv(el, type, fn) {   if(el.removeEventListener) {    el.removeEventListener(type, fn, false);   } else {    el["on" + type] = fn;   }  }   //  获得滑动方向  function getDirection(startX, startY, endX, endY) {   var xRes = startX - endX;   var xResAbs = Math.abs(startX - endX);   var yRes = startY - endY;   var yResAbs = Math.abs(startY - endY);   var direction = "";    if(xResAbs >= yResAbs && xResAbs > 25) {    direction = (xRes > 0) ? "Right" : "Left";   } else if(yResAbs > xResAbs && yResAbs > 25) {    direction = (yRes > 0) ? "Down" : "Up";   }   return direction;  }   //  取得两点之间直线距离  function getDistance(startX, startY, endX, endY) {   return Math.sqrt(Math.pow((startX - endX), 2) + Math.pow((startY - endY), 2));  }   function getLength(pos) {   return Math.sqrt(Math.pow(pos.x, 2) + Math.pow(pos.y, 2));  }   function cross(v1, v2) {   return v1.x * v2.y - v2.x * v1.y;  }   //  取向量  function getVector(startX, startY, endX, endY) {   return(startX * endX) + (startY * endY);  }   //  获取角度  a*b=|a|*|b|*cos(deg);  a*b=x1*x2+y1*y2  function getAngle(v1, v2) {   var mr = getLength(v1) * getLength(v2);   if(mr === 0) {    return 0   };   var r = getVector(v1.x, v1.y, v2.x, v2.y) / mr;   if(r > 1) {    r = 1;   }   return Math.acos(r);  }   //  获取旋转的角度,不是弧度  function getRotateAngle(v1, v2) {   var angle = getAngle(v1, v2);   if(cross(v1, v2) > 0) {    angle *= -1;   }   return angle * 180 / Math.PI;  }   //  包装一个新的事件对象  function wrapEvent(ev, obj) {   var res = {    touches: ev.touches,    type: ev.type   };   if(_typeOf(obj) === "object") {    for(var i in obj) {     res[i] = obj[i];    }   }   return res;  }   //  把伪数组转换成数组  function toArray(list) {   if(list && (typeof list === "object") && isFinite(list.length) && (list.length >= 0) && (list.length === Math.floor(list.length)) && list.length < 4294967296) {    return [].slice.call(list);   }  }   //  判断一个元素列表里面是否有多个元素  function isContain(collection, el) {   if(arguments.length === 2) {    return collection.some(function(elItem) {     return el.isEqualNode(elItem);    });   }   return false;  }   //  生成一个随机id  function uId() {   return Math.random().toString(16).slice(2);  }   //  事件模块  var Event = (function() {    var storeEvents = {};    return {     //  add an event handle    add: function(type, el, handler) {     var selector = el,      len = arguments.length,      finalObject = {},      _type;     /**      * Event.add("swipe", function() {      *      //  ...      * });      */      if(_typeOf(el) === "string") {      el = document.querySelectorAll(el);     }      if(len === 2 && _typeOf(el) === "function") {      finalObject = {       handler: el      };     } else if(len === 3 && el instanceof HTMLElement || el instanceof NodeList && _typeOf(handler) === "function") {      /**       * Event.add("swipe", "#div", function(ev) {       *      //  ...       * });       */      _type = _typeOf(el);      finalObject = {       type: _type,       selector: selector,       el: _type === "nodelist" ? toArray(el) : el,       handler: handler      };     }      if(!storeEvents[type]) {      storeEvents[type] = [];     }      storeEvents[type].push(finalObject);    },     //  remove an event handle    remove: function(type, selector) {     var len = arguments.length;     if(_typeOf(type) === "string" && _typeOf(storeEvents[type]) === "array" && storeEvents[type].length) {      if(len === 1) {       storeEvents[type] = [];      } else if(len === 2) {       storeEvents[type] = storeEvents[type].filter(function(item) {        return !(item.selector === selector || _typeOf(selector) !== "string" && item.selector.isEqualNode(selector));       });      }     }    },     //  trigger an event handle    trigger: function(type, el, argument) {     var len = arguments.length;      /**      * Event.trigger("swipe", document.querySelector("#div"), {      *      //  ...      * });      */     if(len === 3 && _typeOf(storeEvents[type]) === "array" && storeEvents[type].length) {      storeEvents[type].forEach(function(item) {       if(_typeOf(item.handler) === "function") {        if(item.type && item.el) {         argument.target = el;         if(item.type === "nodelist" && isContain(item.el, el)) {          item.handler(argument);         } else if(item.el.isEqualNode && item.el.isEqualNode(el)) {          item.handler(argument);         }        } else {         item.handler(argument);        }       }      });     }    }   };  })();   //  构造函数  function Toucher(selector) {   return new Toucher.fn.init(selector);  }   Toucher.fn = Toucher.prototype = {    //  修改原型构造器   constructor: Toucher,    //  初始化方法   init: function(selector) {    this.el = selector instanceof HTMLElement ? selector :     _typeOf(selector) === "string" ? document.querySelector(selector) : null;    if(_typeOf(this.el) === "null") { //如果没有匹配到     throw new Error("您必须指定一个特定的选择器或特定的DOM对象");    }    this.scale = 1;    this.pinchStartLen = null;    this.isDoubleTap = false;    this.triggedSwipeStart = false;    this.triggedLongTap = false;    this.delta = null;    this.last = null;    this.now = null;    this.tapTimeout = null;    this.singleTapTimeout = null;    this.longTapTimeout = null;    this.swipeTimeout = null;    this.startPos = {};    this.endPos = {};    this.preTapPosition = {};     this.cfg = {     doubleTapTime: 400,     longTapTime: 700    };     //  绑定4个事件    bindEv(this.el, "touchstart", this._start.bind(this));    bindEv(this.el, "touchmove", this._move.bind(this));    bindEv(this.el, "touchcancel", this._cancel.bind(this));    bindEv(this.el, "touchend", this._end.bind(this));    return this;   },    //  提供config方法进行配置   config: function(option) {    if(_typeOf(option) !== "object") {     throw new Error("option 必须是一个JSON的实例对象" + option.toString());    }    for(var i in option) {     this.cfg[i] = option[i];    }    return this;   },    //  on方法绑定事件   /**    * var toucher = Toucher({...});    *    * toucher.on("swipe", function(ev) {    *     //   ...    * });    *    * //   or    *    * toucher.on("tap", "#id", function(ev) {    *     //   ...    * });    *    * support events: singleTap,longTap,swipe,swipeStart,swipeEnd,swipeUp,swipeRight,swipeDown,swipeLeft,pinch,rotate    *    */    on: function(type, el, callback) {    var len = arguments.length;    if(len === 2) {     Event.add(type, el);    } else {     Event.add(type, el, callback);    }    return this;   },    //  off 解除绑定   /**    *  var toucher = Toucher({...});    *  toucher.off(type);    *    *  //  or    *    *  toucher.off(type, selector);    */   off: function(type, selector) {    Event.remove(type, selector);    return this;   },    //  手指刚触碰到屏幕   _start: function(ev) {    if(!ev.touches || ev.touches.length === 0) {     return;    }     var self = this;    var otherToucher, v,     preV = this.preV,     target = ev.target; //获取目标元素     self.now = getTimeStr();  //获取当前时间距1970/1/1时间戳    self.startPos = getPosInfo(ev);  //获取点击的坐标位置信息    self.delta = self.now - (self.last || self.now); //计算时间间隔    self.triggedSwipeStart = false;    self.triggedLongTap = false;     //  快速双击    if(JSON.stringify(self.preTapPosition).length > 2 && self.delta < self.cfg.doubleTapTime && getDistance(self.preTapPosition.clientX, self.preTapPosition.clientY, self.startPos.clientX, self.startPos.clientY) < 25) {     //第一次点击保存了信息内容长度>2,双击时间间隔小于400,两次点击的两点之间直线距离小于半径25的圆圈内     self.isDoubleTap = true;    }     //  长按定时    self.longTapTimeout = setTimeout(function() {     _wrapped = {      el: self.el,      type: "longTap",      timeStr: getTimeStr(),      position: self.startPos     };     Event.trigger("longTap", target, _wrapped);     self.triggedLongTap = true;    }, self.cfg.longTapTime);     //  多个手指放到屏幕    if(ev.touches.length > 1) {     self._cancelLongTap();     otherToucher = ev.touches[1];     v = {      x: otherToucher.pageX - self.startPos.pageX,      y: otherToucher.pageY - self.startPos.pageY     };     this.preV = v;     self.pinchStartLen = getLength(v);     self.isDoubleTap = false;    }     self.last = self.now;    self.preTapPosition = self.startPos;  //保存上一次点击的坐标位置信息     ev.preventDefault();   },    //  手指在屏幕上移动   _move: function(ev) {    if(!ev.touches || ev.touches.length === 0) {     return;    }     var v, otherToucher;    var self = this;    var len = ev.touches.length;    var posNow = getPosInfo(ev);    var preV = self.preV;    var currentX = posNow.pageX;    var currentY = posNow.pageY;    var target = ev.target;     //  手指移动取消长按事件和双击    self._cancelLongTap();    self.isDoubleTap = false;     //  一次按下抬起只触发一次swipeStart    if(!self.triggedSwipeStart) {     _wrapped = {      el: self.el,      type: "swipeStart",      timeStr: getTimeStr(),      position: posNow     };     Event.trigger("swipeStart", target, _wrapped);     self.triggedSwipeStart = true;    } else {     _wrapped = {      el: self.el,      type: "swipe",      timeStr: getTimeStr(),      position: posNow     };     Event.trigger("swipe", target, _wrapped);    }     if(len > 1) {     otherToucher = ev.touches[1];     v = {      x: otherToucher.pageX - currentX,      y: otherToucher.pageY - currentY     };      //  缩放     _wrapped = wrapEvent(ev, {      el: self.el,      type: "pinch",      scale: getLength(v) / this.pinchStartLen,      timeStr: getTimeStr(),      position: posNow     });     Event.trigger("pinch", target, _wrapped);      //  旋转     _wrapped = wrapEvent(ev, {      el: self.el,      type: "rotate",      angle: getRotateAngle(v, preV),      timeStr: getTimeStr(),      position: posNow     });     Event.trigger("rotate", target, _wrapped);     ev.preventDefault();    }     self.endPos = posNow;   },    //  触碰取消   _cancel: function(ev) {    clearTimeout(this.longTapTimeout);    clearTimeout(this.tapTimeout);    clearTimeout(this.swipeTimeout);    clearTimeout(self.singleTapTimeout);   },    //  手指从屏幕离开   _end: function(ev) {    if(!ev.changedTouches) {     return;    }     //  取消长按    this._cancelLongTap();     var self = this;    var direction = getDirection(self.endPos.clientX, self.endPos.clientY, self.startPos.clientX, self.startPos.clientY);    var callback, target = ev.target;     if(direction !== "") {     self.swipeTimeout = setTimeout(function() {      _wrapped = wrapEvent(ev, {       el: self.el,       type: "swipe",       timeStr: getTimeStr(),       position: self.endPos      });      Event.trigger("swipe", target, _wrapped);       //  获取具体的swipeXyz方向      callback = self["swipe" + direction];      _wrapped = wrapEvent(ev, {       el: self.el,       type: "swipe" + direction,       timeStr: getTimeStr(),       position: self.endPos      });      Event.trigger(("swipe" + direction), target, _wrapped);       _wrapped = wrapEvent(ev, {       el: self.el,       type: "swipeEnd",       timeStr: getTimeStr(),       position: self.endPos      });      Event.trigger("swipeEnd", target, _wrapped);     }, 0);    } else if(!self.triggedLongTap) {     self.tapTimeout = setTimeout(function() {      if(self.isDoubleTap) {       _wrapped = wrapEvent(ev, {        el: self.el,        type: "doubleTap",        timeStr: getTimeStr(),        position: self.startPos       });       Event.trigger("doubleTap", target, _wrapped);       clearTimeout(self.singleTapTimeout);       self.isDoubleTap = false;      } else {       self.singleTapTimeout = setTimeout(function() {        _wrapped = wrapEvent(ev, {         el: self.el,         type: "singleTap",         timeStr: getTimeStr(),         position: self.startPos        });        Event.trigger("singleTap", target, _wrapped);       }, 100);      }     }, 0);    }     this.startPos = {};    this.endPos = {};   },    //  取消长按定时器   _cancelLongTap: function() {    if(_typeOf(this.longTapTimeout) !== "null") {     clearTimeout(this.longTapTimeout);    }   }  };   Toucher.fn.init.prototype = Toucher.fn;  //无new 实现   return Toucher;  }));


DEMO:



<!DOCTYPE html> <html lang="en">   <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width,target-densitydpi=high-dpi,initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />   <title></title>   <style type="text/css">    * {     margin: 0;     padding: 0;    }        #toucher {     width: 100%;     height: 400px;     background: yellow;    }   </style>  </head>   <body>   <div id="toucher">   </div>   <div id="result"></div>    <div></div>    <script src="src/Toucher.js"></script>   <script>    window.onload = function() {      var toucher = Toucher("#toucher");     var result = document.querySelector("#result");      toucher.on("singleTap", "#toucher", function(e) {       result.innerHTML = e.type;      })      .on("doubleTap", function(e) {       result.innerHTML = e.type;      })      .on("longTap", function(e) {       result.innerHTML = e.type;      })      .on("swipe", function(e) {       result.innerHTML = e.type;      })      .on("swipeStart", function(e) {       result.innerHTML = e.type;      })      .on("swipeEnd", function(e) {       result.innerHTML = e.type;      })      .on("swipeUp", function(e) {       result.innerHTML = e.type;      })      .on("swipeRight", function(e) {       result.innerHTML = e.type;      })      .on("swipeDown", function(e) {       result.innerHTML = e.type;      })      .on("swipeLeft", function(e) {       result.innerHTML = e.type;      })      .on("rotate", function(e) {       result.innerHTML = e.type + " angle " + e.angle;      })      .on("pinch", function(e) {       result.innerHTML = e.type + " scale " + e.scale;      });    }   </script>  </body>  </html>



BUGs:

部分奇葩手机不支持e.touches,可加在上面最上面库文件的36行处:


// touches  function fnTouches(e) {   if(!e.touches) {    e.touches = e.originalEvent.touches;   }  }