用 JavaScript 实现手势库之第一篇 — 前端组件化

1,547 阅读13分钟

在之前的文章中我们一起实现了一个轮播图的基本效果,我们可以用鼠标去把它来回拖拽。效果上它已经是一个可以做到无尽轮播的轮播图功能了。

但是我们会发现,我们鼠标在图片上任何的动作都会触发到拖拽,并且对图片进行位移的效果。这个其实并不是一个我们最佳实现效果。

在使用鼠标场景下,因为我们鼠标是放在桌面上的,而且鼠标本身也是有一定的重量,所以当我们点击的时候,一般是不会出现任何鼠标的移动信号的。

但是如果是在手机的触屏上,就算我们手再稳,都会出现一定的移位的,这个就是跟我们的手指和屏幕的接触面积、手指是柔软程度等导致的问题。当我们点击发生位移的时候,它发生的时间序列都会是我们之前监听的 downmoveup这样的过程。

在触屏上的事件就是 touchstartmoveend,其实和鼠标的 downmoveup 的性质是差不多的。但是我们也会发现鼠标和触屏是不太一样的,我们前面实现的轮播图还有一个非常大的问题,就是根本没有去支持触屏的。

在现代网页应用开发的场景下,很多时候我们都需要一些功能是可以在电脑和移动端上同时可用的。所以我们是很有必要让我们的轮播图可在触屏和鼠标上有统一的体验,同时也可以让我们的代码去支持基本的手势区分,比如说点一下和划一下等等这些区分。


手势基本知识

所以接下来我们先了解一下手势的一些基本知识,让我们了解到怎么去区分点击和拖拽移动的行为。

这里我们需要使用 startmoveend 这三种场景来进行抽象,同时我们也把鼠标的 downmoveup 也统一抽象到触屏下的 startmoveend 的模型里面。

当然我们还有 pointer 事件,这些也是可以统一抽象到这个模型里面。

我们先来一起整理一下手势体系里面有什么:

  • start
    • 是第一个发生的事件
  • tap
    • 接着我们如果直接点击屏幕,就是一个 tap 的事件
    • 它与 click 是一个比较相似的事件
  • pan start
    • 点击后我们没有把手抬起来,并且发生了一个移动的行为就是一个 pan start
    • 一般来说在识别这个事件的时候,我们会允许一个较低的误差,在 Retina 屏上我们会允许一个 10px 容错范围,如果是一倍屏就是 5px,三倍屏就是 15px。
    • 那么 pan 其实是摄像机领域中的专业词,是移动摄像机的意思。而这里我们表示的是一个比较缓慢的触点推移
  • pan
    • 一开始移动的时候会触发 pan start,之后每一次移动都会触发一个 pan 事件
  • pan end
    • 移动停止时触发的事件
  • flick
    • 如果我们在移动结束的时候达到一定的速度,我们就会认为用户进行了一次清扫或者划动的一个行为,并且会触发一个 flick / swipe 事件。
  • press start
    • 当我们去点击后停留在触屏上的时候,就会触发一个长按的事件
    • 这个长按是通过判定我们停留超过 0.5 秒的时间
    • 超过这个时间我们就判定这个行为是一个按压而不是轻点
    • 这种按压的动作就会触发一些事件,比如说弹出一个菜单
    • 但是如果我们在按压动作的时候发生一个移动 10px 以上的话,就会触发我们之前的 pan start 事件
    • 这种手势是我们去设计手势库的时候经常会遗漏的一种
  • press end
    • 如果我们老老实实按住屏幕,经过一定时间之后手再松开,就会发生一个 press end 的事件
    • 有一些事件也是再这个 press end 的时候触发的,比如说我们有一个按钮,我们控制它是按下去的时候触发,还是按完之后松开的时候触发

这个就是我们基础手势库的体系,当然我们没有包含双指手势。


实现鼠标操作

首先我们来创建一个新的项目目录,然后在这个目录里面创建一个新的 JavaScript 文件。然后我们就在这个文件里面尝试实现我们的手势。

首先还是先去实现一个鼠标的操作。假设我们使用 document.documentElement 取到一个容器的元素,它代表 HTML 元素。

这个时候对获取到的 element 去做事件监听,而在最外层我们就只监听一个 mousedown 的事件。

在 mousedown 的回调函数里面,我们就可以去添加其他的事件。那么这里分别写两个事件的处理函数,一个是 mousemove, 一个就是 mouseup。

这里与我们的 carousel 组件的实现一样,mousemove 和 mosueup 都会在 mousedown 的时候被监听,然后是在 mouseup 的时候被移出。

这里我们不需要给 addEventListener 传第三个参数的。

不过我们可以了解一下,第三个参数到底是什么。这个参数就是 options,而 options 里面可以加三个参数。

  • 一个是用于启用捕捉模式的 capture
  • 一个是控制只触发一次的 once
  • 最后一个就是控制这个里面是否可以 preventDefault,是同步触发还是异步触发的参数叫 passive

然后我们在 mousemove 的函数中打印一下当鼠标点击时的 X 和 Y 坐标。这里使用 event.clientXevent.clientY即可获得这两个坐标。我们也不关心其他相关的信息。

这样我们就完成了 mouse 事件的抽象,具体代码如下:

let element = document.documentElement;

element.addEventListener('mousedown', event => {
  let mousemove = event => {
    console.log(event.clientX, event.clientY);
  };

  let mouseup = event => {
    element.removeEventListener('mousemove', mousemove);
    element.removeEventListener('mouseup', mouseup);
  };

  element.addEventListener('mousemove', mousemove);
  element.addEventListener('mouseup', mouseup);
});

接下来,我们在根目录建立一个 gesture.html 文件,然后引入 gesture.js,最后在浏览器跑起来看看。

当我们点击并且拖动的时候,console 中打印出了一堆数字,这样就证明 mousemove 被正确的触发了。


实现触屏操作

首相我们鼠标中的 mouse 事件是无法被转换成触屏事件的,但是大家经常使用的 click 这类更高级的事件是可以监听到的。

像 mousedown、mouseup、mousemove 这些在移动端都是有独立的触屏事件系列的。

在 touch 系列里面一样有三个对应的事件,touchstarttouchmovetouchend,他们对应的就是 mousedownmousemovemouseup

所以对应的写法就是下面这样:

element.addEventListener('touchstart', event => {});

element.addEventListener('touchmove', event => {});

element.addEventListener('touchend', event => {});

尽管我们看起来鼠标与触屏是比较相似的,但是 Touch 系列的事件与 mouse 系列的事件是不一样的,touch 事件只要我们 start 后,就一定会触发到 move。所以 move 和 start 一定会触发在同一个元素上,不管我们手的位置移动到哪里,这样的话我们就不需要像 mouse 一样,在mousedown 监听开始之后再去监听 mousemove。

这是因为我们鼠标没有按任何按键都是可以在浏览器页面中移动的,而触屏就不一样,我们没有把手指按到屏幕上是无法触发任何移动事件的。

另外的一个不同就是,touch 系列的事件 event 是有多个触点的对象的,这是因为我们触屏上是支持多指手势的。在 event 之中是有一个 changedTouches 这样的事件的,这个里面是一个数组的结构。

在 mouse 系列里面的 event 是可以直接拿到 clientX 和 clientY 的。而在 touch 系列里面的 event 是有多个触点,所以需要在 changedTouches 的数组里面找到某一个触点,再从触点中才能拿到对应的 clientX 和 clientY。

element.addEventListener('touchstart', event => {
  console.log(event.changedTouches);
});

这里我们可以打印出来看一下:

明显看到,调用 event 中的 changedTouches,返回的是一个 TouchList 对象。而这个对象中就有多个 Touch 对象,每一个里面都有这个触点相关的所有信息。

这里面其实还有一个非常神奇的东西,就是 identifier。那么这个 identifier 是用来做什么的呢?其实当我们去触发 touchstart 的时候,我们是可以拿到 touchstart 的这个点的信息的。但是某一个点在 move 的时候,我们没有办法知道是哪一个点在 move,所以我们需要在每一个点需要一个唯一的标识符去追踪它。

所以 touchstart、touchmove 和 touchend 各自都有一个标识符,而这个符就是 touch 的唯一 ID。

知道了 touch 事件的数据结构,我们可以使用这个结构获得与 mouse 事件系列一样的数据。那么如果我们需要获得每一个触点的所在坐标,我们就可以循环 TouchList 数组,访问到每一个 Touch 对象,从中获得它们的 clientX 和 clientY 值。

这里无论是 start、move 还是 end 都是有 changedTouches 这个标志的,所以在每个事件中的 event 都是可以获得它们对应的 changedTouches 中的 TouchList 对象。

element.addEventListener('touchstart', event => {
  for (let touch of event.changedTouches) {
    console.log(touch.clientX, touch.clientY);
  }
});

element.addEventListener('touchmove', event => {
  for (let touch of event.changedTouches) {
    console.log(touch.clientX, touch.clientY);
  }
});

element.addEventListener('touchend', event => {
  for (let touch of event.changedTouches) {
    console.log(touch.clientX, touch.clientY);
  }
});

这样我们就和我们鼠标事件一样,能获得某一个点的坐标。

这里我们需要注意的是,touch 事件里面比鼠标事件多了一个事件叫 touchcancel。那么 cancel 和 end 有什么区别呢?Cancel 表示的是我们手指 touch 的点的序列被异常情况所结束了。

我们正常的序列就是一个 start,接着一堆的 move,最后是一个 end。如果我们代码中加入一个 3 秒的延迟后输出一个 alert 弹窗。那么当我们手指在移动的过程中被弹窗所打断了,这个时候就会出现一个 touchcancel 的事件,而没有出现一个正常的 touchend 的事件。诸如此类的系统操作都有可能会打断我们的 touch 事件的序列,cancel 事件就会替代了 end 事件。

触屏事件是会多出一个 touchcancel 事件,而鼠标事件是没有这样的情况的。


实现通用监听

写到这里我们已经有一个基本的 touch 系列的抽象,接下来我们就要去考虑一下,要怎么写一个通用的 gesture 手势的逻辑。

首先我们想要达到的效果,就是无论用户是用鼠标操作的,还是在移动端使用触屏操作的,都会调用到我们通用的事件,然后我们功能中对应需要响应这些事件的逻辑都会被执行。

那么要实现这样的通用逻辑,我们就需要 4 个通用的函数

  • start()
  • move()
  • end()
  • cancel()

这 4 个函数的参数就是一个点的 point 对象,然后我们就可以在这 4 个函数里面写它的主体逻辑。那么接下来我们就需要去改造我们的鼠标和触屏的监听事件,让它们对应的事件触发的地方调用我们对应的这 4 个函数。

  • mousedown 和 touchstart 就会调用 start 函数
  • mousemove 和 touchmove 就会调用 move 函数
  • mouseup 和 touchend 就会调用 end 函数
  • 最后 touchcancel 就会调用 cancel 函数

最后我们在每一个函数中打印出 point 中的 X 和 Y 坐标。

let element = document.documentElement;

element.addEventListener('mousedown', event => {
  start(event);

  let mousemove = event => {
    move(event);
  };

  let mouseup = event => {
    end(event);
    element.removeEventListener('mousemove', mousemove);
    element.removeEventListener('mouseup', mouseup);
  };

  element.addEventListener('mousemove', mousemove);
  element.addEventListener('mouseup', mouseup);
});

element.addEventListener('touchstart', event => {
  for (let touch of event.changedTouches) {
    start(touch);
  }
});

element.addEventListener('touchmove', event => {
  for (let touch of event.changedTouches) {
    move(touch);
  }
});

element.addEventListener('touchend', event => {
  for (let touch of event.changedTouches) {
    end(touch);
  }
});

element.addEventListener('cancel', event => {
  for (let touch of event.changedTouches) {
    cancel(touch);
  }
});

let start = point => {
  console.log('start', point.clientX, point.clientY);
};
let move = point => {
  console.log('move', point.clientX, point.clientY);
};
let end = point => {
  console.log('end', point.clientX, point.clientY);
};
let cancel = point => {
  console.log('cancel', point.clientX, point.clientY);
};

这个时候我们在浏览器中,可以使用网页和移动端模式测试一下,我们就发现现在无乱是用鼠标拖动还是用移动端的拖动都会打印出当前坐标。

这个就是一个统一的抽象了,这样我们写代码的时候就不用针对具体的鼠标还是 touch 事件去做处理了。我们只需要抽象的去针对每一个点的 startmoveendcancel 等 4种事件类型去写逻辑就可以了。


下一期我们开始来一起实现手势的逻辑哦,记得持续关注三哥哦~

我是来自《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。


⭐️ 三哥推荐

开源项目推荐

Hexo Theme Aurora

在最近的版本 1.4.x 中添加了一位可爱的机器人 Dia

Dia 可以很智能的与你的读者做很多的交互,让你的读者不会在你的博客中迷路,更让你的读者在阅读你的文章的时候增加更多的陪伴和乐趣哦!~

想知道这个 Dia 机器人能做什么,可以去我的博客首页和它玩玩看哦!

最近博主在全面投入开发一个可以 “迈向未来的” Hexo 主题,以极光为主题的博客主题。

如果你是一个开发者,做一个个人博客也是你简历上的一个亮光点。而如果你有一个超级炫酷的博客,那就更加是亮上加亮了,简直就闪闪发光。

如果喜欢这个主题,可以在 Github 上给我点个 🌟 让彼此都发光吧~

主题 Github 地址:github.com/auroral-ui/…

主题使用文档:aurora.tridiamond.tech/zh/


VSCode Aurora Future

对,博主还做了一个 Aurora 的 VSCode 主题。用了Hexo Theme Aurora 相对应的颜色色系。这个主题的重点特性的就只用了 3 个颜色,减少在写代码的时候被多色多彩的颜色所转移了你的注意力,让你更集中在写代码之中。

喜欢的大家可以支持一下哦! 直接在 VSCode 的插件搜索中输入 “Aurora Future” 即可找到这个主题哦!~

主题 Github 地址:github.com/auroral-ui/…

主题插件地址:marketplace.visualstudio.com/items?itemN…


Firefox Aurora Future

我不知道大家,但是最近我在用火狐浏览器来做开发了。个人觉得火狐还真的是不错的。推荐大家尝试一下。

当然我这里想给大家介绍的是我在火狐也做了一个 Aurora 主题。对的!用的是同一套的颜色体系。喜欢的小伙伴可以试一下哦!

主题地址:addons.mozilla.org/en-US/firef…


博主直播间

博主开始在B站直播工作和学习,欢迎过来《直播间》围观和一起听音乐哦。

我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!

学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و