接上两篇文章《移动端的touch事件》http://www.lizhiqianduan.com/blog/index.php/2018/06/07/mobile-multi-touch/ ,
以及《手势的判断条件》http://www.lizhiqianduan.com/blog/index.php/2018/06/27/condition-of-guesture/,看了这两篇文章的读者朋友或许已经自己写出了一个手势库了。
这篇文章,我就来分享一下我自己写的一个手势库。
这里是我写的一个示例链接:http://www.lizhiqianduan.com/products/ycc/examples/multi-touch/,此链接需在移动设备上查看。
新建Gesture类绑定一个HTML元素
看过之前博客的读者,都应该知道,手势其实就是通过某个HTML元素的touchstart、touchmove、touchend事件来模拟的。
而这些事件的回调大多都包含Touch对象,Touch对象有一个target属性,这个属性是用来表明当前触摸的HTML元素的。
所以,我们这个手势库需要一个HTML进行绑定,之后所有的手势都是在这个HTML元素上触发的。大致如下:
/*
* @param option
* @param option.target 手势触发的HTML对象
* @extends Ycc.Listener
* @constructor
*/
Ycc.Gesture = function (option) {
Ycc.Listener.call(this);
this.option = option;
};
Ycc.Gesture.prototype = new Ycc.Listener()
这个Ycc是我目前正在写的一个项目,这里我们的Gesture类继承了Listerner类。
这个Listener类主要功能是事件的监听和触发,便于我们的Gesture类监听和触发Gesture的自定义事件。
这样定义之后,我们监听手势触发就非常方便了。只需要如下即可:
var demo = new Ycc.Gesture({target:document.body});
demo.ontap = function (touch) {
//todo ...
};
demo.ondoubletap = function (touch) {
//todo ...
};
demo.onzoom = function (touch) {
//todo ...
};
demo.onrotate = function (touch) {
//todo ...
};
Gesture类初始化
有了我们的类之后,我们需要类的初始化函数,我设计的大致结构如下:
/*
*初始化函数
* @private
*/
Ycc.Gesture.prototype._init = function () {
var self = this;
var tracer = new Ycc.TouchLifeTracer(
{target:this.option.target}
);
tracer.onlifestart = function (life){
// todo ...
};
tracer.onlifechange = function (life){
// todo ...
};
tracer.onlifeend = function (life){
// todo ...
};
};
这里有个Ycc.TouchLifeTracer,它是一个触摸点生命周期的一个追踪模块。
它的主要功能是对接触HTML元素的每个触摸点,从开始接触到接触结束的跟踪。
它的实现,我们在前面的文章中也已经提到了,这里不清楚的读者请翻看一下本文开头的两篇文章。
接下来,我就来简单讲解各手势的实现。
tap手势的实现
有了上面的这个追踪模块,我们的Gesture类的实现会容易得多。
只需要在各个生命周期内根据手势的判断条件触发事件即可。
tap手势的判断条件如下:
1、触摸过程中只有一个接触点
2、触摸时间小于某个阈值,一般是300ms
3、触摸过程中不能存在移动事件
那么对应在我们的追踪器tracer中实现即可,那么初始化函数_init里的内容大致如下:
// Gesture引用
var self = this;
// 是否阻止事件触发
var prevent = {
tap:false
};
tracer.onlifestart = function (life) {
// 条件1:多个触摸点的情况,不触发tap事件
if(tracer.currentLifeList.length>1){
prevent.tap = true;
};
};
tracer.onlifechange = function (life) {
// 条件1:多个触摸点的情况,不触发tap事件
if(tracer.currentLifeList.length>1){
prevent.tap = true;
};
// 条件2:触摸过程中存在移动事件,且大于10px,则不触发tap
var firstMove = life.startTouchEvent;
var lastMove = Array.prototype.slice.call(life.moveTouchEventList,-1)[0];
if(Math.abs(lastMove.pageX-firstMove.pageX)>10 || Math.abs(lastMove.pageY-firstMove.pageY)>10){
prevent.tap=true;
}
};
tracer.onlifeend = function (life) {
// 条件1:接触结束,个数为0
if(tracer.currentLifeList.length===0){
// 条件3:tap的时间不能超过300ms
if(!prevent.tap && life.endTime-life.startTime<300){
// 触发tap
self.triggerListener('tap',life.endTouchEvent);
}
}
};
doubletap手势的实现
doubletap手势的判断条件如下:
1、存在两次tap事件
2、两次tap事件的x、y坐标必须在某个阈值内,一般是10px
3、两次tap事件的时间间隔必须在某个阈值内,一般是300ms
它是建立在tap之上的,只需要在触发tap的时候判断doubletap条件即可。
所以其初始化函数_init里的内容大致如下:
// Gesture引用
var self = this;
// 是否阻止事件触发
var prevent = {
tap:false
};
// 两次点击的生命周期
var preLife,curLife;
tracer.onlifestart = function (life) {
if(tracer.currentLifeList.length>1){
prevent.tap = true;
};
};
tracer.onlifechange = function (life) {
if(tracer.currentLifeList.length>1){
prevent.tap = true;
};
var firstMove = life.startTouchEvent;
var lastMove = Array.prototype.slice.call(life.moveTouchEventList,-1)[0];
if(Math.abs(lastMove.pageX-firstMove.pageX)>10 || Math.abs(lastMove.pageY-firstMove.pageY)>10){
prevent.tap=true;
}
};
tracer.onlifeend = function (life) {
if(tracer.currentLifeList.length===0){
if(!prevent.tap && life.endTime-life.startTime<300){
// 触发tap
self.triggerListener('tap',life.endTouchEvent);
// 只需在这里进行处理
// 两次点击在300ms内,并且两次点击的范围在10px内,则认为是doubletap事件
if(preLife
&& life.endTime-preLife.endTime<300
&& Math.abs(preLife.endTouchEvent.pageX-life.endTouchEvent.pageX)<10
&& Math.abs(preLife.endTouchEvent.pageY-life.endTouchEvent.pageY)<10)
{
// 触发doubletap
self.triggerListener('doubletap',life.endTouchEvent);
preLife = null;
return this;
}
preLife = life;
}
}
};
旋转rotate和缩放zoom手势的实现
它们的判断条件一样,这里放在一起说
1、触摸过程中至少有两个接触点,实际中也是取最先接触的两个触摸点进行计算
2、触摸过程中存在移动
其初始化函数_init里的内容大致如下:
// Gesture引用
var self = this;
// 是否阻止事件触发
var prevent = {
tap:false
};
// 两次点击的生命周期
var preLife,curLife;
tracer.onlifestart = function (life) {
// 条件1:存在多个接触点
if(tracer.currentLifeList.length>1){
prevent.tap = true;
// 缩放、旋转只取最先接触的两个点
preLife = tracer.currentLifeList[0];
curLife = tracer.currentLifeList[1];
return this;
};
};
tracer.onlifechange = function (life) {
// 条件2:多个点存在移动
if(tracer.currentLifeList.length>1){
prevent.tap = true;
// 获取旋转角度和缩放比例
var rateAndAngle = self.getZoomRateAndRotateAngle(preLife,curLife);
// 触发zoom事件
if(Ycc.utils.isNum(rateAndAngle.rate)){
self.triggerListener('zoom',rateAndAngle.rate);
}
// 触发rotate事件
if(Ycc.utils.isNum(rateAndAngle.angle)){
self.triggerListener('rotate',rateAndAngle.angle);
}
};
};
tracer.onlifeend = function (life) {
// 与lifeend无关
};
上面代码中,最神奇的或许是getZoomRateAndRotateAngle函数了。
它主要功能是根据两个接触点的位置信息,获取旋转角度和缩放比例。
它只不过是用到了一些数学上的方法。
如下,
缩放比例=当前距离/初始距离
旋转角度=初始向量和当前向量的夹角
其大致实现,如下:
Ycc.Gesture.prototype.getZoomRateAndRotateAngle = function (preLife, curLife) {
// 初始坐标
var x0=preLife.startTouchEvent.pageX,
y0=preLife.startTouchEvent.pageY,
x1=curLife.startTouchEvent.pageX,
y1=curLife.startTouchEvent.pageY;
var preMoveTouch = preLife.moveTouchEventList.length>0?preLife.moveTouchEventList[preLife.moveTouchEventList.length-1]:preLife.startTouchEvent;
var curMoveTouch = curLife.moveTouchEventList.length>0?curLife.moveTouchEventList[curLife.moveTouchEventList.length-1]:curLife.startTouchEvent;
// 当前坐标
var x0move=preMoveTouch.pageX,
y0move=preMoveTouch.pageY,
x1move=curMoveTouch.pageX,
y1move=curMoveTouch.pageY;
// 初始向量
var vector0 = new Ycc.Math.Vector(x1-x0,y1-y0),
// 当前向量
vector1 = new Ycc.Math.Vector(x1move-x0move,y1move-y0move);
// 计算夹角
var angle = Math.acos(vector1.dot(vector0)/(vector1.getLength()*vector0.getLength()))/Math.PI*180;
return {
// 计算缩放比例
rate:vector1.getLength()/vector0.getLength(),
// 向量叉乘,判断夹角正负号
angle:angle*(vector1.cross(vector0).z>0?-1:1)
};
};
数学基础不好的读者朋友,就不用想了,直接复制过去吧。
其他手势
略。
其他手势相对来说比较简单,根据我们的判断条件,有了生命周期追踪,能很方便的实现。
这里不再分享,感兴趣的读者朋友请参看Ycc项目源码中手势模块:https://github.com/lizhiqianduan/ycc/tree/develop
结尾
还有很多手势是我们这个库里没有的,读者可以根据这个思路自行扩展。只要判断条件明确,按照文章这个思路还是很好实现的。
附:
Ycc.Gesture完整代码:https://github.com/lizhiqianduan/ycc/blob/develop/src/Ycc.Gesture.class.js