用 JavaScript 实现手势库 — 事件派发与 Flick 事件前端组件化

Posted 三钻

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用 JavaScript 实现手势库 — 事件派发与 Flick 事件前端组件化相关的知识,希望对你有一定的参考价值。

前端《组件化系列》目录

我们上一期已经实现了所有的 gesture(手势),接下来我们需要实现的就是事件派发的功能。

事件派发

在 DOM 里面事件的派发是使用 new Event , 然后在上面加一些属性,最后把这个事件给派发出去的。

所以我们这里也是一样,建立一个 dsipatch 的函数,并且加入 typeproperty 这些参数。这里的 property 含有 context 对象和 point 坐标两个属性。

在我们的 dispatch 函数中,首先我们需要做的就是创建一个 event 对象。在新的浏览器 API 中,我们可以直接使用 new Event 来创建。当然我们也可以使用自定义事件来创建 new CustomEvent。那么我们这里,就用普通的 new Event 就好了。

function dispatch(type, properties) 
  let event = new Event(type);

然后我们循环一下 properties 这个对象,把里面的属性都抄写一下。然后我们新创建的 event 是需要挂在一个元素上面,把它挂在到我们之前定义的 element 上即可。

function dispatch(type, properties) 
  let event = new Event(type);
  for (let name in properties) 
    event[name] = properties[name];
  
  element.dispatchEvent(event);

这里其实还有一个问题,就是我们之前写的监听都是挂载在 element 之上的。最后我们要把这些都换成挂载在 document 上。

element.addEventListener('mousedown', event => 
  let context = Object.create(null);
  contexts.set(`mouse$1 << event.button`, context);

  start(event, context);

  let mousemove = event => 
    let button = 1;

    while (button <= event.buttons) 
      if (button & event.buttons) 
        let key;
        // Order of buttons & button is not the same
        if (button === 2) 
          key = 4;
         else if (button === 4) 
          key = 2;
         else 
          key = button;
        

        let context = contexts.get('mouse' + key);
        move(event, context);
      
      button = button << 1;
    
  ;

  let mouseup = event => 
    let context = contexts.get(`mouse$1 << event.button`);
    end(event, context);
    contexts.delete(`mouse$1 << event.button`);

    if (event.buttons === 0) 
      document.removeEventListener('mousemove', mousemove);
      document.removeEventListener('mouseup', mouseup);
      isListeningMouse = false;
    
  ;

  if (!isListeningMouse) 
    document.addEventListener('mousemove', mousemove);
    document.addEventListener('mouseup', mouseup);
    isListeningMouse = true;
  
);

然后我们来把 end 函数中的 tap 事件 dipatch(派发)出来试试:

let end = (point, context) => 
  if (context.isTap) 
    //console.log('tap');
    // 把原先的 console.log 换成 dispatch 调用
    // 这个事件不需要任何特殊属性,直接传`空对象`即可
    dispatch('tap', )
    clearTimeout(context.handler);
  

  if (context.isPan) 
    console.log('pan-end');
  

  if (context.isPress) 
    console.log('press-end');
  
;

那么最后,我们可以尝试在 html 中加入一个脚本,在里面监听一下我们新创建的 tap 事件。

<script src="gesture.js"></script>
<body oncontextmenu="event.preventDefault()"></body>
<script>
  document.documentElement.addEventListener('tap', () => 
    console.log('Tapped!');
  );
</script>

这个时候,如果我们去浏览器上点击一下,就会触发我们的 tap 事件,并且输出我们的 'Tapped' 消息了!

这样我们的派发事件就大功告成了。

实现一个 flick 事件

这里我们一起来完成最后一个最特别的 flick 事件。Flick 事件在我们所有的事件体系里是比较特殊的,因为它是一个需要判断数独的一个事件。

根据我们前面讲到的,在 pan start 之后,如果我们在手指离开屏幕之前,我们执行了一个快速滑动手指的动作,到达一定的速度以上就会触发我们的 flick 事件,而不是原本的 pan end 的事件。

那么需要如何判断这个速度的?其实可以在我们的 move 函数中,获得当前这一次移动时的速度。但是这个并不能帮助我们去处理,因为如果只按照两个点之间移动时的速度,根据浏览器实现的不同,它会有一个较大的误差。

所以更加准确的方式就是,取数个点,然后用它们之间的平均值作为判定的值。那么要实现这个功能,我们就需要存储一段时间之内的这些点,然后使用这些点来计算出速度的平均值。

有了实现的思路了,我们就来整理下,在代码中怎么去编写这一块的逻辑。

首先我们需要在触发 start 的时候,就把第一个记录点加入到我们的全局 context 之中。而这里需要记录几个值:

  • t:代表当前点触发/加入时的时间,这里我们使用 Date.now()
  • x:代表当前点 x 轴的坐标
  • y:代表当前点 y 轴的坐标

这些值到了后面都会用来计算移动速度的。

let start = (point, context) => 
  (context.startX = point.clientX), (context.startY = point.clientY);

  context.points = [
    
      t: Date.now(),
      x: point.clientX,
      y: point.clientY,
    ,
  ];

  context.isPan = false;
  context.isTap = true;
  context.isPress = false;

  context.handler = setTimeout(() => 
    context.isPan = false;
    context.isTap = false;
    context.isPress = true;
    console.log('press-start');
    context.handler = null;
  , 500);
;

然后每一次触发 move 的时候,都给当前的 content 放入一个新的点。但是在加入新的点之前,需要过滤一次已经存储的点。我们只需要最近 500 毫秒内的点来计算速度即可,其余的点就可以过滤掉了。

在执行 flick 动作的时候,我们是不会滑动一个很长的距离和时间的,加上我们是需要捕捉一个快速的滑动动作,这个动作肯定是在 500 毫秒以内的动作,要不也不叫 “快” 了。所以这里就只需要 500 毫秒内的点即可。

let move = (point, context) => 
  let dx = point.clientX - context.startX,
    dy = point.clientY - context.startY;

  if (!context.isPan && dx ** 2 + dy ** 2 > 100) 
    context.isPan = true;
    context.isTap = false;
    context.isPress = false;
    console.log('pan-start');
    clearTimeout(context.handler);
  

  if (context.isPan) 
    console.log(dx, dy);
    console.log('pan');
  

  context.points = context.points.filter(point => Date.now() - point.t < 500);

  context.points.push(
    t: Date.now(),
    x: point.clientX,
    y: point.clientY,
  );
;

在 end 事件触发的时候,就可以来计算这次滑动的速度了。因为这里是计算用户滑动时的速度,如果用户是其他类型的手势动作,是不需要去计算速度的。所以这段计算逻辑就可以写在 isPan 成立的判断里面即可。

首先给这个手势动作一个状态变量 isFlick,并且给予它一个默认值为 false

在计算速度之前,一样需要过滤一次我们 context 中储存的全部的点,把 500 毫秒之外的点过滤掉。

在数学或者物理中,有一个计算速度的公式: 速度 = 距离 / 用时。那么这里要去计算速度的话,首先需要计算的就是距离。而这里要计算的是直径距离,所以需要 x 轴和 y 轴的距离的二次幂相加,然后开根号获得的值就是我们要的直径距离。

那么 x 轴距离为例,就是当前点的 x 轴坐标,减去记录中第一个点的 x 轴左边。y 轴的距离就同理可得了。那么有了距离,我们就可以直接从当前点和第一个点的时间差获得 用时。最后就可以运算出速度。

let end = (point, context) => 
  context.isFlick = false;

  if (context.isTap) 
    //console.log('tap');
    // 把原先的 console.log 换成 dispatch 调用
    // 这个事件不需要任何特殊属性,直接传`空对象`即可
    dispatch('tap', );
    clearTimeout(context.handler);
  

  if (context.isPan) 
    context.points = context.points.filter(point => Date.now() - point.t < 500);

    let d = Math.sqrt((point.x - context.points[0].x) ** 2 + (point.y - context.points[0].y) ** 2);
    let v = d / (Date.now() - context.points[0].t);
  

  if (context.isPress) 
    console.log('press-end');
  
;

好样的,这样我们就有两个点之间的 v 速度。那么现在呢,我们需要知道多快的速度才能认为是一个 flick 动作呢?这里就用上帝视角直接得出 1.5 像素每毫秒的速度就是最合适的(这个怎么算出来的?其实我们可以直接 console.log(v),把速度打印出啦,然后我们手动去测试,就会发现大概 v = 1.5 的时候差不多就是对的了)。

所以我们这里直接就可以判断, 如果 v > 1.5 的话,我们就认为用户的手势就是一个 flick,否则就是普通的 pan-end。

let end = (point, context) => 
  context.isFlick = false;

  if (context.isTap) 
    //console.log('tap');
    // 把原先的 console.log 换成 dispatch 调用
    // 这个事件不需要任何特殊属性,直接传`空对象`即可
    dispatch('tap', );
    clearTimeout(context.handler);
  

  if (context.isPan) 
    context.points = context.points.filter(point => Date.now() - point.t < 500);

    let d = Math.sqrt((point.x - context.points[0].x) ** 2 + (point.y - context.points[0].y) ** 2);
    let v = d / (Date.now() - context.points[0].t);

    if (v > 1.5) 
      context.isFlick = true;
      dispatch('flick', );
     else 
      context.isFlick = false;
      dispatch('panend', );
    
  

  if (context.isPress) 
    console.log('press-end');
  
;

这样 flick 事件的处理就完成了,其实这段代码中还有一些 console.log() 是没有被改为使用 dispatch 给派发出去的。但是接下来就要开始看看怎么重新封装这个手势库了,所以这里我们就不一一更改过来先了。

如果想把这里的代码写完整的同学,可以自行把所有的 console.log(事件名) 部分的代码都改正过来哦~

最后附上到此完整的代码。

let element = document.documentElement;

let contexts = new Map();

let isListeningMouse = false;

element.addEventListener('mousedown', event => 
  let context = Object.create(null);
  contexts.set(`mouse$1 << event.button`, context);

  start(event, context);

  let mousemove = event => 
    let button = 1;

    while (button <= event.buttons) 
      if (button & event.buttons) 
        let key;
        // Order of buttons & button is not the same
        if (button === 2) 
          key = 4;
         else if (button === 4) 
          key = 2;
         else 
          key = button;
        

        let context = contexts.get('mouse' + key);
        move(event, context);
      
      button = button << 1;
    
  ;

  let mouseup = event => 
    let context = contexts.get(`mouse$1 << event.button`);
    end(event, context);
    contexts.delete(`mouse$1 << event.button`);

    if (event.buttons === 0) 
      document.removeEventListener('mousemove', mousemove);
      document.removeEventListener('mouseup', mouseup);
      isListeningMouse = false;
    
  ;

  if (!isListeningMouse) 
    document.addEventListener('mousemove', mousemove);
    document.addEventListener('mouseup', mouseup);
    isListeningMouse = true;
  
);

element.addEventListener('touchstart', event => 
  for (let touch of event.changedTouches) 
    let context = Object.create(null);
    contexts.set(event.identifier, context);
    start(touch, context);
  
);

element.addEventListener('touchmove', event => 
  for (let touch of event.changedTouches) 
    let context = contexts.get(touch.identifier);
    move(touch, context);
  
);

element.addEventListener('touchend', event => 
  for (let touch of event.changedTouches) 
    let context = contexts.get(touch.identifier);
    end(touch, context);
    contexts.delete(touch.identifier);
  
);

element.addEventListener('cancel', event => 
  for (let touch of event.changedTouches) 
    let context = contexts.get(touch.identifier);
    cancel(touch, context);
    contexts.delete(touch.identifier);
  
);

let start = (point, context) => 
  (context.startX = point.clientX), (context.startY = point.clientY);

  context.points = [
    
      t: Date.now(),
      x: point.clientX,
      y: point.clientY,
    ,
  ];

  context.isPan = false;
  context.isTap = true;
  context.isPress = false;

  context.handler = setTimeout(() => 
    context.isPan = false;
    context.isTap = false;
    context.isPress = true以上是关于用 JavaScript 实现手势库 — 事件派发与 Flick 事件前端组件化的主要内容,如果未能解决你的问题,请参考以下文章

用 JavaScript 实现手势库 — 事件派发与 Flick 事件前端组件化

用 JavaScript 实现手势库 — 事件派发与 Flick 事件前端组件化

用 JavaScript 实现手势库 — 事件派发与 Flick 事件前端组件化

用 JavaScript 实现手势库 — 封装手势库前端组件化

用 JavaScript 实现手势库 — 封装手势库前端组件化

用 JavaScript 实现手势库 — 封装手势库前端组件化