干货:实现一个H5平台的手势库

接上两篇文章《移动端的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​

你可能感兴趣的:(干货:实现一个H5平台的手势库)