用javascript实现手势库—事件派发与flick事件前端组件化(代码片段)

三钻 三钻     2023-03-31     677

关键词:

前端《组件化系列》目录

我们上一期已经实现了所有的 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事件前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—手势动画应用前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—封装手势库前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—封装手势库前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—手势逻辑前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现手势库-实现监听逻辑「六」用JavaScript实现手势库—手势逻辑《本期》…待续…上一期《实现监听逻辑》中我们一起实现了基... 查看详情

用javascript实现手势库—手势动画应用前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—手势动画应用前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—手势动画应用前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

用javascript实现手势库—支持多键触发前端组件化(代码片段)

...立Markup组件风格「三」用JSX实现Carousel轮播组件「四」用JavaScript实现时间轴与动画「五」用JavaScript实现三次贝塞尔动画库-前端组件化「六」用JavaScript实现手势库-实现监听逻辑「七」用JavaScript实现手势库—手势逻辑「八」用Java... 查看详情

javascript实现按键精灵

...派发事件,执行操作。关于事件的更多细节,可以参考《JavaScript中事件处理》1、模拟MouseEvent中的click事件,x与y位置随机点击2、模拟TouchEvent中的touchstart和touchmove,用scroll来做滑动效果3、模拟 查看详情

用javascript实现手势库-实现监听逻辑前端组件化(代码片段)

在之前的文章中我们一起实现了一个轮播图的基本效果,我们可以用鼠标去把它来回拖拽。效果上它已经是一个可以做到无尽轮播的轮播图功能了。但是我们会发现,我们鼠标在图片上任何的动作都会触发到拖拽,并... 查看详情

javascript事件事件类型之触摸与手势事件

一、触摸事件touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。touchmove:当手指在屏幕上滑动时连续地触发。在这个世界发生期间,调用preventDefault()可以阻止滚动。touchend:当手指在屏幕上移开时... 查看详情

javascript触摸与手势事件

处理Touch事件能让你跟踪用户的每一根手指的位置。你可以绑定以下四种Touch事件:    1.touchstart: //手指放到屏幕上的时候触发     2.touchmove: //手指在屏幕上移动的时候触发   &nb... 查看详情

事件派发机制的实现

//事件派发机制的实现varEventDispatcher={addEventListener:function(eventType,handler){//事件的存储if(!this.eventMap){this.eventMap={}}//对每个事件,允许添加多个监听if(!this.eventMap[eventType]){this.eventMap[eventType]=[]}//把回调函数 查看详情

eventemitter事件派发器(代码片段)

...居然出现过几次了,说明它还是很重要的,就学了一下,JavaScript源代码还是挺好理解的。   对于Event事件大家应该都很熟悉,比如dom中的button,可以通过addEventListener/attachEvent(IE)添加click事件处理。而一般的object对象是... 查看详情

实现一个javascript手势库--base-gesture.js

...这些东西就感觉网页会库不少呢~~(舒服)。当然啦。原生javascript并没有为我们提供这些花里胡哨的东西,需要我们自己去实现下喽。又当然,,现在还是有许多js手势库的,比如hammer.js 查看详情

android之利用jsbridge库实现html,javascript与android的所有交互

java和js互通框架WebViewJavascriptBridge是移动UIView和Html交互通信的桥梁,用作者的话来说就是实现java和js的互相调用的桥梁。替代了WebView的自带的JavascriptInterface的接口,使得我们的开发更加灵活和安全。本博客把JSBridge库近所有Andro... 查看详情

javascript学习之烟花特效实现(面向对象)(代码片段)

烟花特效的实现本特效使用面向对象编程分析OOA点击触发事件烟花运动分成两个阶段向上飞爆炸OODclassFireWorkconstructor()bindEvent()let_this=this;ele.onclick=function()//fly结束再去调用boom函数_this.fly(_this.boom);fly(boom)boom()CSS设计实 查看详情