首页 > 代码库 > 一个最小手势库的实现
一个最小手势库的实现
众所周知,浏览器暴露了四个事件给开发者,touchstart touchmove touchend touchcancel,在这四个事件的回调函数可以拿到TouchEvent。
TouchEvent:
touches:当前位于屏幕上的所有手指动作的列表
targetTouches:位于当前 DOM 元素上的手指动作的列表
changedTouches:涉及当前事件的手指动作的列表
TouchEvent里可以拿到各个手指的坐标,那么可编程性就这么产生了。
Tap点按
移动端click有300毫秒延时,tap的本质其实就是touchend。但是要判断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。那么用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动呢?可以根据上面三个判断得出,具体的代码如下:
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。
如上图所示,两点之间的距离比值求pinch的scale。这个scale会挂载在event上,让用户反馈给dom的transform或者其他元素的scale属性。
rotate旋转
如上图所示,利用内积,可以求出两次手势状态之间的夹角θ。但是这里怎么求旋转方向呢?那么就要使用差乘(Vector Cross)。
利用cross结果的正负来判断旋转的方向。
cross本质其实是面积,可以看下面的推导:
所以,物理引擎里经常用cross来计算转动惯量,因为力矩其实要是力乘矩相当于面积:
总结
主要的一些事件触发原理已经在上面讲解,还有如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="http://www.mamicode.com/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,可:
// touches function fnTouches(e) { if(!e.touches) { e.touches = e.originalEvent.touches; } }
一个最小手势库的实现