干货:实现一个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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • -- iOS事件全面解析 概览 iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机其实...
    翘楚iOS9阅读 2,938评论 0 13
  • 在开发过程中,大家或多或少的都会碰到令人头疼的手势冲突问题,正好前两天碰到一个类似的bug,于是借着这个机会了解了...
    闫仕伟阅读 5,294评论 2 23
  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 56,596评论 51 597
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,987评论 4 26
  • 1.健康不是指没有疾病,而是一种身心完整、社会适应良好的状态。 ——世界卫生组织对健康...
    微澜细语阅读 184评论 0 4