• 一个最小手势库的实现


    众所周知,浏览器暴露了四个事件给开发者,touchstart touchmove touchend touchcancel,在这四个事件的回调函数可以拿到TouchEvent。
    TouchEvent:
    touches:当前位于屏幕上的所有手指动作的列表
    targetTouches:位于当前 DOM 元素上的手指动作的列表
    changedTouches:涉及当前事件的手指动作的列表
    TouchEvent里可以拿到各个手指的坐标,那么可编程性就这么产生了。

    Tap点按

    图片2

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

    longTap长按

    longtap

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

    swipe划

    swipe

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

    pinch捏

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

    QQ截图20161111093632

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

    rotate旋转

    QQ截图20161111093842

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

    cross_show

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

    QQ截图20161111094608

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

    QQ截图201611110946526

    总结

    主要的一些事件触发原理已经在上面讲解,还有如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 {
    				 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;
    		}
    	}
    

      

      

     

  • 相关阅读:
    asp.net core启动源码以及监听,到处理请求响应的过程
    HTTP缓存
    【SpringData&JPA从入门到精通】02JPA API
    关于取地址&和解引用*的小问题
    多态性
    ASP.NET Core WebApi返回结果统一包装实践
    后端思维之数据库性能优化方案
    【项目】项目194
    lambda 表达式
    在APICloud开发平台使用友盟统计功能教程
  • 原文地址:https://www.cnblogs.com/libin-1/p/6129310.html
Copyright © 2020-2023  润新知