一个最小手势库的实现

/**
 * myHand.js
 * by name:libin 
 */

"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=""></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>
复制代码

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

// touches
function fnTouches(e) {
    if(!e.touches) {
        e.touches = e.originalEvent.touches;
    }
}
复制代码




对于手势库总结性的分析,可以看作者自己写的这篇,超小Web手势库AlloyFinger原理

我所写的,可能就更多是实现的细节,怎么实现一个具体的功能,而不是这总架构性的分析,能力的限制造成大局观的限制吧。

Tag

point-1: 手势库的整体架构方式
point-2: 如何实现旋转操作的判断,怎么计算旋转的夹角和旋转的方向
point-3: 具体手势,伪代码逻辑

代码架构方式

var AlloyFinger = function(el, option) {
    // 获取el元素

    // 改变start, move, end, cancel函数的this指向
    this.start = this.start.bind(this);

    // 设置 touchstart 的响应函数 -> start
    // touchmove, touchend, touchcancel类似

    // 绑定自定义手势的响应函数( option中获取 )
   
    // 标志位变量
} 
AlloyFinger.prototype = {
    start: function(){...},
    move: function(){...},
    end: function(){...},
    cancel: function(){...}
}复制代码

总的来说,将所有的属性和方法,都挂载在了AlloyFinger对象下面。

在touch原生事件的四个阶段,分别绑定自己的响应函数,在响应函数中,进行一系列的判断,来确定是否要触发某个手势。

我能说刚开始看的时候,感觉这代码简直写的6吗:)。还是自己能力太渣了,很多东西还欠缺。
比如像我这种新手,如果没看过源代码,会怎么实现这些功能?可能写出来的手势库,响应函数只能绑定一个。然后触发手势的时候,直接调用func( )。里面手势的逻辑判断,也会渣的一匹。。。

匿名立即执行函数前加分号

; (function(){...})()复制代码

以前都没有注意,看网上的答案,总结来说就是,防止在压缩时,前面一行没有分号,和后面一行合并时就会出现错误。

判断旋转手势方向及角度

设向量 A( x1, y1 ), B( x2, y2 )
向量点乘:A · B = x1 * x2 + y1 * y2 = |a| * |b| * cosθ
向量差乘:A x B = x1 * y2 - x2 * y1 = |a| * |b| * sinθ

你说这有什么用啊,那么,当进行图片旋转操作的时候,你怎么计算它旋转的角度和方向呢:)

角度:
由点乘公式,可以导出等式 cosθ = x1 * x2 + y1 * y2/ (|a| * |b|), 然后利用Math.acos函数就可以获得θ。
而对于向量A 和 B 的模长,勾股定理求得。

旋转方向:

  1. 对于坐标轴中,逆时针旋转的方向才是正方向。
  2. A x B,表示向量A旋转到向量B
  3. 差乘值 > 0,逆时针旋转,取 -θ. 差乘值 < 0, 顺时针旋转,取θ。
  4. 旋转角度要转化为 弧度制的值

参考文章

综上,旋转操作要同时用到点乘和叉乘,分别获取旋转角度的大小和方向。再看 getLen(), dot(), getAngle(), cross(), getRotateAngle() 这几个函数基本上就没啥问题了

绑定多个响应函数

有兴趣可以看HandlerAdmin的实现,类似于观察者模式。利用一个数组保存这个手势的所有响应函数,增加add, del, dispatch方法进行响应函数的增删、触发,很熟悉的套路。

标志位用途

this.delta = null;  // 两次点击时间差,用于判断是否是双击
this.last = null; // 上一次点击时,touchstart时间点
this.now = null;  // 当前点击,touchstart时间点
this.preTapPosition = { x: null, y: null };    // 上次点击的点
this.isDoubleTap = false; // 是否双击标志位

// 定时器,便于后期cancel取消响应函数的执行
this.tapTimeout = null;
this.singleTapTimeout = null;
this.longTapTimeout = null;
this.swipeTimeout = null;

// x1,y2 touchstart时的点坐标
// x2, y2 touchmove过程中的点坐标——》 最后停止的位置
this.x1 = this.x2 = this.y1 = this.y2 = null;

// 多点操作用(缩放,旋转), 计算两个手指按下点,所连成的向量
this.preV = { x: null, y: null };

this.pinchStartLen = null;  // 缩放用,计算缩放比例
this.scale = 1; // 缩放比例复制代码

手势分析

start: 
    1. 获取当前时间和位置,计算时间差

    2. dispatch touchStart事件

    3. 如果存在preTapPosition
        3.1 判断时间差间隔,两次点击区域范围
        3.2 设置双击标志位

    4. 将当前点设置为preTapPosition

    5. 判断是否多点触摸
        5.1 计算两点连成的向量preV距离
        5.2 dispatch multipointStart事件

    6. 设置longTap定时器
        6.1 时间到后,dispatch longTap事件

move: 
    1. currentX, currentY获取当前move位置,设置临时变量preV

    3. 判断多点触摸
        是
        3.1.1 move后,两点形成向量V, 计算V距离
        3.1.2 通过preV与V计算缩放比例
        3.1.3 dispatch scale事件
        3.1.4 计算旋转角度 dispatch rotate事件

        否
        3.2.1 计算deltaX, deltaY
        3.2.3 dispatch pressMove事件
    
    4. 清除longTap定时器

    5. 设置最终点x2, y2

end: 
    1. 取消longTap定时器

    2. 如果touches.length<2, dispatch multipointEnd事件

    3. dispatch touchEnd事件

    4. 通过起始点x1, y1; 终点x2, y2,计算距离大小
        距离大于30,响应swipe
        4.1.1 计算swipe方向direction
        4.1.2 设置swipe定时器
                dispatch swipe事件

        距离小于30, 设置tap定时器
        4.2.1 dispatch tap事件
        4.2.2 判断isDoubleTap,
                是
                1. dispatch doubleTap事件
                2. 清除singleTap定时器

                否
                1. 设置singleTap定时器
                    dispatch singleTap事件

    5. this.preV.x = 0; this.preV.y = 0;
       this.scale = 1;
       this.pinchStartLen = null;
       this.x1 = this.x2 = this.y1 = this.y2 = null; 

cancel:
    1. 清除longTap, singleTap, swipe, tap定时器
    2. dispatch touchcancel事件复制代码

事件间隔为0的 Timeout

setTimeout(function () {
  console.log('Timeout start runing...');
}, 0)
for(var i=0; i<5; i++) {
  console.log(i);
}复制代码

结果:

0
1
2
3
4
Timeout start runing...复制代码

所以,timeout为0, 不代表就不能被cancel掉的。如果不理解的话,可以看下这本书,javascript异步编程


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值