众所周知,浏览器暴露了四个事件给开发者,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="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;
}
}