博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一个最小手势库的实现
阅读量:6332 次
发布时间:2019-06-22

本文共 13720 字,大约阅读时间需要 45 分钟。

众所周知,浏览器暴露了四个事件给开发者,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。那么用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动呢?可以根据上面三个判断得出,具体的代码如下:

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:

 

			

 

BUGs:

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

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

  

  

 

转载地址:http://ekioa.baihongyu.com/

你可能感兴趣的文章
Poj2245--Lotto(DFS)
查看>>
MicrosoftSQLServer数据库定时备份(备份计划)的几种方式
查看>>
java基础Synchronized关键字之对象锁
查看>>
Node js 嵌入式模板引擎 ejs 的使用
查看>>
LVS+Keepalive+Nginx实现负载均衡
查看>>
支付宝即时到账DEMO配置与使用
查看>>
oracle 12c rac vip和监听故障
查看>>
不规则ROI的提取
查看>>
算法导论系列:分治算法
查看>>
16-Flutter移动电商实战-切换后页面状态的保持AutomaticKeepAliveClientMixin
查看>>
asp.net 生成静态页面
查看>>
2102: [Usaco2010 Dec]The Trough Game
查看>>
3212: Pku3468 A Simple Problem with Integers
查看>>
SVG 的使用
查看>>
网商银行×OceanBase:首家云上银行的分布式数据库应用实践
查看>>
ES 6大纲总结——Array的扩展
查看>>
用Pyenv 和 Virtualenv 搭建单机多版本 Python 虚拟开发环境
查看>>
esxi安装全过程及基本配置(转)
查看>>
排序--插入
查看>>
HTML简介
查看>>