• 如何实现swipe、tap、longTap等自定义事件


    前言

    移动端原生支持touchstarttouchmovetouchend等事件,但是在平常业务中我们经常需要使用swipetapdoubleTaplongTap等事件去实现想要的效果,对于这种自定义事件他们底层是如何实现的呢?让我们从Zepto.jstouch模块去分析其原理。您也可以直接查看touch.js源码注释

    源码仓库

    原文链接

    touch

    事件简述

    Zepto的touch模块实现了很多与手势相关的自定义事件,分别是swipe, swipeLeft, swipeRight, swipeUp, swipeDown,doubleTap, tap, singleTap, longTap
    事件名称 事件描述
    swipe 滑动事件
    swipeLeft ←左滑事件
    swipeRight →右滑事件
    swipeUp ↑上滑事件
    swipeDown ↓下滑事件
    doubleTap 双击事件
    tap 点击事件(非原生click事件)
    singleTap 单击事件
    longTap 长按事件
    ;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(eventName){
      $.fn[eventName] = function(callback){ return this.on(eventName, callback) }
    })
    

    可以看到Zepto把这些方法都挂载到了原型上,这意味着,你可以直接用简写的方式例如$('body').tap(callback)

    前置条件

    在开始分析这些事件如何实现之前,我们先了解一些前置条件
    • 部分内部变量
    var touch = {},
        touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
        // 长按事件定时器时间
        longTapDelay = 750,
        gesture
    

    touch: 用以存储手指操作的相关信息,例如手指按下时的位置,离开时的坐标等。

    touchTimeout,tapTimeout, swipeTimeout,longTapTimeout分别存储singleTap、tap、swipe、longTap事件的定时器。

    longTapDelay:longTap事件定时器延时时间

    gesture: 存储ieGesture事件对象

    • 滑动方向判断(swipeDirection)

    我们根据下图以及对应的代码来理解滑动的时候方向是如何判定的。需要注意的是浏览器中的“坐标系”和数学中的坐标系还是不太一样,Y轴有点反过来的意思。

    手机屏幕坐标图

    /**
      * 判断移动的方向,结果是Left, Right, Up, Down中的一个
      * @param  {} x1 起点的横坐标
      * @param  {} x2 终点的横坐标
      * @param  {} y1 起点的纵坐标
      * @param  {} y2 终点的纵坐标
      */
    
    function swipeDirection(x1, x2, y1, y2) {
      /**
        * 1. 第一个三元运算符得到如果x轴滑动的距离比y轴大,那么是左右滑动,否则是上下滑动
        * 2. 如果是左右滑动,起点比终点大那么往左滑动
        * 3. 如果是上下滑动,起点比终点大那么往上滑动
        * 需要注意的是这里的坐标和数学中的有些不一定 纵坐标有点反过来的意思
        * 起点p1(1, 0) 终点p2(1, 1)
        */
      return Math.abs(x1 - x2) >=
        Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
    }
    
    • 触发长按事件
    function longTap() {
      longTapTimeout = null
      if (touch.last) {
        // 触发el元素的longTap事件
        touch.el.trigger('longTap')
        touch = {}
      }
    }
    

    在触发长按事件之前先将longTapTimeout定时器取消,如果touch.last还存在则触发之,为什么要判断touch.last呢,因为swip, doubleTap,singleTap会将touch对象置空,当这些事件发生的时候,自然不应该发生长按事件。

    • 取消长按,以及取消所有事件
    // 取消长按
    function cancelLongTap() {
      if (longTapTimeout) clearTimeout(longTapTimeout)
      longTapTimeout = null
    }
    
    // 取消所有事件
    
    function cancelAll() {
      if (touchTimeout) clearTimeout(touchTimeout)
      if (tapTimeout) clearTimeout(tapTimeout)
      if (swipeTimeout) clearTimeout(swipeTimeout)
      if (longTapTimeout) clearTimeout(longTapTimeout)
      touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null
      touch = {}
    }

    方式都是类似,先调用clearTimeout取消定时器,然后释放对应的变量,等候垃圾回收。

    整体结构分析

    
    
    $(document).ready(function(){
      /**
        * now 当前触摸时间
        * delta 两次触摸的时间差
        * deltaX x轴变化量
        * deltaY Y轴变化量
        * firstTouch 触摸点相关信息
        * _isPointerType 是否是pointerType
        */
      var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType
    
      $(document)
        .bind('MSGestureEnd', function(e){
          // xxx 先不看这里
        })
        .on('touchstart MSPointerDown pointerdown', function(e){
          // xxx 关注这里
        })
        .on('touchmove MSPointerMove pointermove', function(e){
          // xxx 关注这里
        })
        .on('touchend MSPointerUp pointerup', function(e){
          // xxx 关注这里
        })
        .on('touchcancel MSPointerCancel pointercancel', cancelAll)
    
        $(window).on('scroll', cancelAll)
      })
    
    

    这里将详细代码暂时省略了,留出整体框架,可以看出Zepto在dom,ready的时候在document上添加了MSGestureEnd,touchstart MSPointerDown pointerdown,touchmove MSPointerMove pointermove,touchcancel MSPointerCancel pointercancel等事件,最后还给在window上加了scroll事件。我们将目光聚焦在touchstart,touchmove,touchend对应的逻辑,其他相对少见的事件在暂不讨论

    touchstart

    if((_isPointerType = isPointerEventType(e, 'down')) 
    && !isPrimaryTouch(e)) return
    

    要走到touchstart事件处理程序后续逻辑中,需要先满足一些条件。到底是哪些条件呢?先来看看isPointerEventType, isPrimaryTouch两个函数做了些什么。

    **isPointerEventType

    function isPointerEventType(e, type){
      return (e.type == 'pointer'+type ||
        e.type.toLowerCase() == 'mspointer'+type)
    }
    

    Pointer Event相关知识点击这里

    isPrimaryTouch

    function isPrimaryTouch(event){
      return (event.pointerType == 'touch' ||
        event.pointerType == event.MSPOINTER_TYPE_TOUCH)
        && event.isPrimary
    }
    

    根据mdn pointerType,其类型可以是mouse,pen,touch,这里只处理其值为touch并且isPrimary为true的情况。

    接着回到

    if((_isPointerType = isPointerEventType(e, 'down')) 
    && !isPrimaryTouch(e)) return
    

    其实就是过滤掉非触摸事件。

    触摸点信息兼容处理

    // 如果是pointerdown事件则firstTouch保存为e,否则是e.touches第一个
    firstTouch = _isPointerType ? e : e.touches[0]
    

    这里只清楚e.touches[0]的处理逻辑,另一种不太明白,望有知晓的同学告知一下,感谢感谢。

    复原终点坐标

    // 一般情况下,在touchend或者cancel的时候,会将其清除,如果用户调阻止了默认事件,则有可能清空不了,但是为什么要将终点坐标清除呢?
    if (e.touches && e.touches.length === 1 && touch.x2) {
      // Clear out touch movement data if we have it sticking around
      // This can occur if touchcancel doesn't fire due to preventDefault, etc.
      touch.x2 = undefined
      touch.y2 = undefined
    }
    

    存储触摸点部分信息

    // 保存当前时间
    now = Date.now()
    // 保存两次点击时候的时间间隔,主要用作双击事件
    delta = now - (touch.last || now)
    // touch.el 保存目标节点
    // 不是标签节点则使用该节点的父节点,注意有伪元素
    touch.el = $('tagName' in firstTouch.target ?
      firstTouch.target : firstTouch.target.parentNode)
    // touchTimeout 存在则清除之,可以避免重复触发
    touchTimeout && clearTimeout(touchTimeout)
    // 记录起始点坐标(x1, y1)(x轴,y轴)
    touch.x1 = firstTouch.pageX
    touch.y1 = firstTouch.pageY
    

    判断双击事件

    // 两次点击的时间间隔 > 0 且 < 250 毫秒,则当做doubleTap事件处理
    if (delta > 0 && delta <= 250) touch.isDoubleTap = true
    

    处理长按事件

    // 将now设置为touch.last,方便上面可以计算两次点击的时间差
    touch.last = now
    // longTapDelay(750毫秒)后触发长按事件
    longTapTimeout = setTimeout(longTap, longTapDelay)
    

    touchmove

    .on('touchmove MSPointerMove pointermove', function(e){
      if((_isPointerType = isPointerEventType(e, 'move')) &&
        !isPrimaryTouch(e)) return
      firstTouch = _isPointerType ? e : e.touches[0]
      // 取消长按事件,都移动了,当然不是长按了
      cancelLongTap()
      // 终点坐标 (x2, y2)
      touch.x2 = firstTouch.pageX
      touch.y2 = firstTouch.pageY
      // 分别记录X轴和Y轴的变化量
      deltaX += Math.abs(touch.x1 - touch.x2)
      deltaY += Math.abs(touch.y1 - touch.y2)
    })
    

    手指移动的时候,做了三件事情。

    1. 取消长按事件
    2. 记录终点坐标
    3. 记录x轴和y轴的移动变化量

    touchend

    .on('touchend MSPointerUp pointerup', function(e){
      if((_isPointerType = isPointerEventType(e, 'up')) &&
        !isPrimaryTouch(e)) return
      // 取消长按事件  
      cancelLongTap()
      // 滑动事件,只要X轴或者Y轴的起始点和终点的距离超过30则认为是滑动,并触发滑动(swip)事件,
      // 紧接着马上触发对应方向的swip事件(swipLeft, swipRight, swipUp, swipDown)
      // swipe
      if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
          (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
    
        swipeTimeout = setTimeout(function() {
          if (touch.el){
            touch.el.trigger('swipe')
            touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
          }
          touch = {}
        }, 0)
      // touch对象的last属性,在touchstart事件中添加,所以触发了start事件便会存在  
      // normal tap
      else if ('last' in touch)
        // don't fire tap when delta position changed by more than 30 pixels,
        // for instance when moving to a point and back to origin
        // 只有当X轴和Y轴的变化量都小于30的时候,才认为有可能触发tap事件
        if (deltaX < 30 && deltaY < 30) {
          // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
          // ('tap' fires before 'scroll')
          tapTimeout = setTimeout(function() {
    
            // trigger universal 'tap' with the option to cancelTouch()
            // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
            // 创建自定义事件
            var event = $.Event('tap')
            // 往自定义事件中添加cancelTouch回调函数,这样使用者可以通过该方法取消所有的事件
            event.cancelTouch = cancelAll
            // [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
            // 当目标元素存在,触发tap自定义事件
            if (touch.el) touch.el.trigger(event)
    
            // trigger double tap immediately
            // 如果是doubleTap事件,则触发之,并清除touch
            if (touch.isDoubleTap) {
              if (touch.el) touch.el.trigger('doubleTap')
              touch = {}
            }
    
            // trigger single tap after 250ms of inactivity
            // 否则在250毫秒之后。触发单击事件
            else {
              touchTimeout = setTimeout(function(){
                touchTimeout = null
                if (touch.el) touch.el.trigger('singleTap')
                touch = {}
              }, 250)
            }
          }, 0)
        } else {
          // 不是tap相关的事件
          touch = {}
        }
        // 最后将变化量信息清空
        deltaX = deltaY = 0
    
    })
    

    touchend事件触发时,相应的注释都在上面了,但是我们来分解一下这段代码。

    swip事件相关

    if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
      (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
    
    swipeTimeout = setTimeout(function() {
      if (touch.el){
        touch.el.trigger('swipe')
        touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
      }
      touch = {}
    }, 0)
    

    手指离开后,通过判断x轴或者y轴的位移,只要其中一个跨度大于30便会触发swip及其对应方向的事件。

    tap,doubleTap,singleTap

    这三个事件可能触发的前提条件是touch对象中还存在last属性,从touchstart事件处理程序中知道last在其中记录,而在touchend之前被清除的时机是长按事件被触发longTap,取消所有事件被调用cancelAll

    if (deltaX < 30 && deltaY < 30) {
      // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
      // ('tap' fires before 'scroll')
      tapTimeout = setTimeout(function() {
    
        // trigger universal 'tap' with the option to cancelTouch()
        // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
        var event = $.Event('tap')
        event.cancelTouch = cancelAll
        // [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
        if (touch.el) touch.el.trigger(event)
      }    
    }

    只有当x轴和y轴的变化量都小于30的时候才会触发tap事件,注意在触发tap事件之前,Zepto还将往事件对象上添加了cancelTouch属性,对应的也就是cancelAll方法,即你可以通过他取消所有的touch相关事件。

    
    // trigger double tap immediately
    
    if (touch.isDoubleTap) {
      if (touch.el) touch.el.trigger('doubleTap')
      touch = {}
    }
    
    // trigger single tap after 250ms of inactivity
    
    else {
      touchTimeout = setTimeout(function(){
        touchTimeout = null
        if (touch.el) touch.el.trigger('singleTap')
        touch = {}
      }, 250)
    }
    
    

    在发生触发tap事件之后,如果是doubleTap,则会紧接着触发doubleTap事件,否则250毫秒之后触发singleTap事件,并且都会讲touch对象置为空对象,以便下次使用

    // 最后将变化量信息清空
    deltaX = deltaY = 0
    

    touchcancel

    
    .on('touchcancel MSPointerCancel pointercancel', cancelAll)
    

    touchcancel被触发的时候,取消所有的事件。

    scroll

    
    $(window).on('scroll', cancelAll)
    

    当滚动事件被触发的时候,取消所有的事件(这里有些不解,滚动事件触发,完全有可能是要触发tap或者swip等事件啊)。

    结尾

    最后说一个面试中经常会问的问题,touch击穿现象。如果对此有兴趣可以查看移动端click延迟及zepto的穿透现象, [新年第一发--深入不浅出zepto的Tap击穿问题
    ](https://zhuanlan.zhihu.com/p/...

    参考

    1. 移动端click延迟及zepto的穿透现象
    2. [新年第一发--深入不浅出zepto的Tap击穿问题

    ](https://zhuanlan.zhihu.com/p/...

    1. 读Zepto源码之Touch模块
    2. pointerType
    3. [[翻译]整合鼠标、触摸 和触控笔事件的Html5 Pointer Event Api](https://juejin.im/post/594e06...

    文章目录

    本文转载于:猿2048→https://www.mk2048.com/blog/blog.php?id=hbka1kjchaa

  • 相关阅读:
    关于WorkFlow的使用以及例子
    11 个用来创建图形和图表的 JavaScript 工具包
    产品经理看程序员的自我修养
    extern "C" 的作用
    DLL 演示
    C++中L和_T()之区别
    VMware:Configuration file was created by a VMware product with more features than this version
    使用内存映射来对文件排序
    平衡二叉树的插入删除操作
    volatile关键字的使用
  • 原文地址:https://www.cnblogs.com/10manongit/p/12822228.html
Copyright © 2020-2023  润新知