资讯专栏INFORMATION COLUMN

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

罗志环 / 3422人阅读

摘要:分别存储事件的定时器。事件定时器延时时间存储事件对象滑动方向判断我们根据下图以及对应的代码来理解滑动的时候方向是如何判定的。取消长按,以及取消所有事件取消长按取消所有事件方式都是类似,先调用取消定时器,然后释放对应的变量,等候垃圾回收。

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

源码仓库

原文链接

事件简述
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)
})

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

取消长按事件

记录终点坐标

记录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/...
参考

移动端click延迟及zepto的穿透现象

[新年第一发--深入不浅出zepto的Tap击穿问题

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

读Zepto源码之Touch模块

pointerType

[[翻译]整合鼠标、触摸 和触控笔事件的Html5 Pointer Event Api](https://juejin.im/post/594e06...

文章目录

touch.js

如何实现swipe、tap、longTap等自定义事件 (2017-12-22)

ie.js

Zepto源码分析之ie模块(2017-11-03)

data.js

Zepto中数据缓存原理与实现(2017-10-03)

form.js

Zepto源码分析之form模块(2017-10-01)

zepto.js

这些Zepto中实用的方法集(2017-08-26)

Zepto核心模块之工具方法拾遗 (2017-08-30)

看Zepto如何实现增删改查DOM (2017-10-2)

Zepto这样操作元素属性(2017-11-13)

向Zepto学习关于"偏移"的那些事(2017-12-10)

event.js

mouseenter与mouseover为何这般纠缠不清?(2017-06-05)

向Zepto.js学习如何手动触发DOM事件(2017-06-07)

谁说你只是"会用"jQuery?(2017-06-08)

ajax.js

原来你是这样的jsonp(原理与具体实现细节)(2017-06-11)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93115.html

相关文章

  • 读Zepto源码之Touch模块

    摘要:在触发事件前,先将保存定时器的变量释放,如果对象中存在,则触发事件,保存的是最后触摸的时间。如果有触发的定时器,清除定时器即可阻止事件的触发。其实就是清除所有相关的定时器,最后将对象设置为。进入时,立刻清除定时器的执行。 大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zepto 的 touch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swip...

    Prasanta 评论0 收藏0
  • 微信小程序 自制手势库

    摘要:微信小程序手势事件库由于微信小程序只能够支持时间,对于比较复杂的事件只能自己实现因此自己对库进行了改造,开发了时候微信小程序手势事件库使用进行编写手势库支持以下事件仓库地址点我点我使用由于和微信小程序强绑定,因此需要在元素上面绑定好所有的事 WxTouchEvent 微信小程序手势事件库 由于微信小程序只能够支持 tap,longtap,touchstart,touchmove,tou...

    cucumber 评论0 收藏0
  • mTouch移动端 ( 兼容pc端) 手势操作库

    摘要:移动端兼容端手势操作库,支持的事件单击双击长按滑动开始滑动结束滑动向左划向右划向上划向下划提供的接口配置项单击事件允许的滑动距离双击事件的延时时长两次单击的最大时间间隔长按事件的最小时长触发方向滑动的最小距离触发方向滑动允许的最长时长以上是 mTouch mTouch移动端 ( 兼容pc端) 手势操作库,view on github 支持的事件: tap 单击 doubletap ...

    dadong 评论0 收藏0

发表评论

0条评论

罗志环

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<