移动端手势库设计与实践

前言

本次给你们分享的是常见的移动端单点触摸事件的设计思路及实践。node

核心技术

主要就是利用移动端的如下3个触摸事件,来模拟和实现自定义的手势操做git

  • touchstart:手指触摸到屏幕的一瞬间触发
  • touchmove:手指在屏幕上移动时触发
  • touchend:手指从屏幕上离开时触发

概念梳理

touch事件触发时,有3组数据能够得到触摸信息,可能你们会对这几组数据有些混淆,我根据本身的理解来尽可能用通俗的语言给你们解释清楚github

  • touches:整个屏幕上全部的触摸点集合
  • targetTouches:当前DOM元素上的全部触摸点集合
  • changedTouches:相对上一次触摸点发生变化的集合

咱们先来看一张图数组

如图所示,咱们在节点B上绑定touch事件,圆圈表明触摸点。浏览器

此时节点B有3个触摸点,即targetTouches数组有3项,分别储存着触摸点的信息,此时touchestargetTouches是相同的。动画

当咱们将手指3移出节点B(始终保持3个手指触摸在屏幕上),那么touchmove事件触发,targetTouches只剩2项,而touches依然有3项,此时changedTouches只有一项(由于只有手指3改变了)。spa

而后咱们让全部手指离开屏幕,那么此时touchend事件触发,touches只剩0项,targetTouches剩0项,changedTouches有3项(由于3个手指发生了变化)。设计

好了,理解这些概念,有助于咱们理解代码中什么时候改去哪一个touch数组里面的值。code

思路及实践

tap

tap能够理解为点击事件,和click不一样的是,移动端的click事件有大约300ms的延迟,这是由于浏览器要判断是否为双击事件。blog

思路

  • touchstart:时记录时间点以及触摸点的x、y坐标
  • touchend:计算此时与开始时的时间差,水平和垂直方向的偏移量

说明:时间差用来判断用户触摸的时长,超过规定时间则tap事件无效;偏移量用来判断用户的触摸事件内是否有过移动的痕迹,这里咱们容许少许的偏移,由于手指可能出现抖动的状况

实现

const tapDefaults = {
  time: 250,
  offset: 10
}

export default function tap (node, a, b) {
  let st, sx, sy
  let opts, callback

  if (typeof a === 'function') {
    callback = a
    opts = Object.assign({}, tapDefaults, b)
  } else {
    callback = b
    opts = Object.assign({}, tapDefaults, a)
  }

  node.addEventListener('touchstart', (e) => {
    e.preventDefault() // 组织浏览器默认行为,防止触摸过程页面滚动

    const touch = e.targetTouches[0]
    st = e.timeStamp
    sx = touch.pageX
    sy = touch.pageY
  }, false)

  node.addEventListener('touchend', (e) => {
    const touch = e.changedTouches[0]

    if (
      // 若为长按,则将时间断定条件更改
      e.timeStamp - st <= opts.time &&
      Math.abs(touch.pageX - sx) <= opts.offset &&
      Math.abs(touch.pageY - sy) <= opts.offset
    ) {
      callback && callback()
    }
  }, false)
}

doubletap

即双击事件,两次点击时间间隔不超过规定时间则视为有效。

思路

  • 第一次有效点击,记录该状态,反之重置状态
  • 第二次有效点击,触发事件并重置状态
  • 若两次时间间隔过长,重置状态

实现

const tapDefaults = {
  time: 250,
  offset: 10
}

function handler (node, inject) {
  let st, sx, sy

  node.addEventListener('touchstart', (e) => {
    e.preventDefault()

    const touch = e.targetTouches[0]
    st = e.timeStamp
    sx = touch.pageX
    sy = touch.pageY
  }, false)

  node.addEventListener('touchend', (e) => {
    const touch = e.changedTouches[0]

    inject({
      time: e.timeStamp - st,
      offsetX: Math.abs(touch.pageX - sx),
      offsetY: Math.abs(touch.pageY - sy)
    })
  }, false)
}

export function doubletap (node, a, b) {
  let opts, callback
  let status = 0

  if (typeof a === 'function') {
    callback = a
    opts = Object.assign({}, tapDefaults, b)
  } else {
    callback = b
    opts = Object.assign({}, tapDefaults, a)
  }

  handler(node, (info) => {
    if (
      info.time <= opts.time &&
      info.offsetX <= opts.offset &&
      info.offsetY <= opts.offset
    ) {
      if (status === 0) {
        status = 1
        // 时间间隔太长则重置状态
        setTimeout(() => {
          status = 0
        }, opts.time)
      } else if (status === 1) {
        callback && callback()
        status = 0
      }
    } else {
      status = 0
    }
  })
}

longtap

即长按,手指按住超过规定时间视为有效,在手指离开时触发。

思路

  • 和tap事件思路同样,只不过期间的断定条件变动一下,改成超过多长时间才触发

实现

const longtapDefaults = {
  time: 350,
  offset: 10
}

// 这里代码逻辑和tap事件同样
// 更改时间断定为:
// e.timeStamp - st > opts.time

press

即按压事件,按住超过规定时间自动触发,注意和longtap不一样的是,longtap须要等到手指离开时触发,而press在按压时间达到规定值,自动触发,此时手指还在屏幕上。

思路

  • touchstart:记录此时的x、y坐标,而且开启一个定时器,在规定时间后执行回调,默认是350ms
  • touchmove:监听移动过程,在事件触发前,若出现偏移量过大,则取消定时器
  • touchend:取消定时器

分析:根据以上思路,若按压时间短,则手指离开时定时器已取消,回调不会触发。

实现

const pressDefaults = {
  time: 350,
  offset: 10
}

export default function press (node, a, b) {
  let opts, callback, sx, sy
  let timer = null

  if (typeof a === 'function') {
    callback = a
    opts = Object.assign({}, pressDefaults, b)
  } else {
    callback = b
    opts = Object.assign({}, pressDefaults, a)
  }

  node.addEventListener('touchstart', (e) => {
    e.preventDefault()

    const touch = e.targetTouches[0]
    sx = touch.pageX
    sy = touch.pageY

    timer = setTimeout(() => {
      callback && callback()
    }, opts.time)
  }, false)

  node.addEventListener('touchmove', (e) => {
    const touch = e.targetTouches[0]

    if (
      Math.abs(touch.pageX - sx) > opts.offset ||
      Math.abs(touch.pageY - sy) > opts.offset
    ) {
      clearTimeout(timer)
    }
  }, false)

  node.addEventListener('touchend', () => {
    clearTimeout(timer)
  }, false)
}

swipe

即手指滑动事件,应用场景如:轮播图左右滑动切换,整屏页面滑动翻页等,算是移动端最多见的手势之一了。

思路

  • touchstart:记录时间点和触摸点位置
  • touchmove:实时判断滑动偏移量
  • touchend:计算滑动速度和方向,条件断定是否触发事件

分析:考虑到须要在滑动过程作一些动画特效等操做,所以咱们将滑动中的事件暴露给用户自定义,值得注意的是,如果要实时改变滑块位置的话,最好不要截流或防抖,截流会形成滑动卡顿的现象,而防抖会出现延迟同步滑动操做的状况;另外对滑动速度也进行了处理,原则上用户滑动距离超过规定后即视为有效,然而为了更好的用户体验,咱们断定,若是用户在短期内滑动速度很是快的话,也视为一次有效的操做,不必定非要滑动很长的距离

实现

const swipeDefaults = {
  direction: 'horizontal', // vertical
  speed: 200,
  offset: 100,
  prevent: true,
  // touchmove: (offset) => {}
}

export default function swipe (node, a, b) {
  let opts, callback, sTime, sTouch, eTouch

  if (typeof a === 'function') {
    callback = a
    opts = Object.assign({}, swipeDefaults, b)
  } else {
    callback = b
    opts = Object.assign({}, swipeDefaults, a)
  }

  node.addEventListener('touchstart', (e) => {
    if (opts.prevent) {
      e.preventDefault()
    }

    sTime = e.timeStamp
    sTouch = eTouch = e.targetTouches[0]
  }, false)

  if (typeof opts.touchmove === 'function') {
    node.addEventListener('touchmove', (e) => {
      eTouch = e.targetTouches[0]

      if (opts.direction === 'horizontal') {
        opts.touchmove(eTouch.pageX - sTouch.pageX)
      } else {
        opts.touchmove(eTouch.pageY - sTouch.pageY)
      }
    }, false)
  }

  node.addEventListener('touchend', (e) => {
    eTouch = e.changedTouches[0]

    let time = e.timeStamp - sTime
    let offset, direction

    if (opts.direction === 'horizontal') {
      offset = eTouch.pageX - sTouch.pageX
      direction = offset > 0 ? 'right' : 'left'
    } else {
      offset = eTouch.pageY - sTouch.pageY
      direction = offset > 0 ? 'down' : 'up'
    }

    if (
      Math.abs(offset) >= opts.offset ||
      Math.abs(offset) / time * 1000 >= opts.speed
    ) {
      callback && callback(direction)
    }
  }, false)
}

结束语

经过以上的思路讲解和代码实现,咱们完成了一个单点触控的移动端手势库,是否是火烧眉毛的想要一睹为快、体验一番。

最后附上本次分享的源码和文档:https://github.com/ansenhuang/axe/blob/master/packages/touch/README.md

相关文章
相关标签/搜索