博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
如何实现swipe、tap、longTap等自定义事件
阅读量:5879 次
发布时间:2019-06-19

本文共 9949 字,大约阅读时间需要 33 分钟。

前言

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

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}

根据,其类型可以是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.pageXtouch.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 immediatelyif (touch.isDoubleTap) {  if (touch.el) touch.el.trigger('doubleTap')  touch = {}}// trigger single tap after 250ms of inactivityelse {  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击穿现象。如果对此有兴趣可以查看 , [新年第一发--深入不浅出zepto的Tap击穿问题
](

参考

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

](

  1. [[翻译]整合鼠标、触摸 和触控笔事件的Html5 Pointer Event Api](

文章目录

  • touch.js

    • (2017-12-22)
  • ie.js

    • (2017-11-03)
  • data.js

    • (2017-10-03)
  • form.js

    • (2017-10-01)
  • zepto.js

    • (2017-08-26)
    • (2017-08-30)
    • (2017-10-2)
    • (2017-11-13)
    • (2017-12-10)
  • event.js

    • (2017-06-05)
    • (2017-06-07)
    • (2017-06-08)
  • ajax.js

    • (2017-06-11)

转载地址:http://wjcix.baihongyu.com/

你可能感兴趣的文章
mariadb启动报错:[ERROR] Can't start server : Bind on unix socket: Permission denied
查看>>
nginx的信号量
查看>>
《携程的技术演进之路》读后感
查看>>
股票新闻速递 隐私声明
查看>>
LeetCode--206--反转链表
查看>>
matlab list函数参数,Matlab 函数参数汇总
查看>>
云im php,网易云IM
查看>>
测试linux vsftpd,vsftpd配置、测试
查看>>
河南农业大学c语言平时作业答案,河南农业大学2004-2005学年第二学期《C语言程序设计》期末考试试卷(2份,有答案)...
查看>>
c语言打开alist文件,C语言 文件的打开与关闭详解及示例代码
查看>>
c语言 中的共用体和结构体如何联合定义,结构体(Struct)、联合体(Union)和位域
查看>>
iPad pro能运行c语言吗,关于iPad Pro的五大槽点 你必须知道
查看>>
wordcount源代码c语言,Word Count程序(C语言实现)
查看>>
( 译、持续更新 ) JavaScript 上分小技巧(一)
查看>>
CI框架 -- URI 路由
查看>>
RPi 2B Raspbian system install
查看>>
HTTP 响应
查看>>
Android显示GIF图片
查看>>
区分SQL Server关联查询之inner join,left join, right join, full outer join并图解
查看>>
mysql日志详细解析
查看>>