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

三钻 三钻     2022-12-06     144

关键词:

前端《组件化系列》目录

经历了多次的迭代,我们的手势库功能都已经实现了。但是到了这里我们的代码确实需要重新整理和封装了。如果同学们还记得的,我们之间一开始获取的元素 element 是写死的。但是作为一个手势库,我们绑定的元素必然是由这个库的使用者而决定的。

有一些同学可能就会问:“为什么不一开始就想好怎么写,一开始就封装好呢?现在实现了所有功能,再回头去封装,不是重复工作,浪费了时间了吗?”

其实如果我们一开始我们就想怎么封装,应该怎么设计这个库。因为设计需要考虑的因素很多,而且要实现的功能还没有落实。其实往往这个时候设计出来的方案或者架构到了后面都会被修改 N 次的。那么最后我们花在设计的时间就会比我们花在实现这些功能上要多得多。但是如果我们是先实现了功能,然后再去封装,就变得简单的多。

所以接下来我们就开始封装这个手势库吧!

要封装这个手势库,第一件事就是列出现有的函数,并且给他们归类。那么我们的这个手势库的其实就 3 个部分

  • Listener 监听器
    • mouse 事件
      • mousedown
      • mouseup
      • mousemove
    • touch 事件
      • touchstart
      • touchmove
      • touchend
      • cancel
  • recognizer 识别器
    • start()
    • move()
    • end()
  • dispatcher 分发器
    • dispatch()

如果我们想把这个库做成一个 API 的话,我们就可以用上面提到的三个部分来解耦。

按照我们上面写的 3 个部分来看,其实他们是有串联关系,甚至是嵌套关系的。首先我们需要实例一个 Listener 监听器。然后这个 Listener 需要有一个 Recognizer 识别器,用来识别监听到的事件。最后我们的识别器需要有一个 Dispatcher 派发器,被识别的事件会通过派发器分发出去。

所以最后我们调用这个手势 API 的方式应该是这样的:

new Listener(new Recognizer(new Dispatcher()))

Listener 监听器

那么我们来看看怎么实现 Listener

因为一个 Listener 实例会默认传入一个 Recognizer,所以我们先建立一个 contructor 构造函数,让它接收传入的 Recognizer。Listener 也需要知道它监听的元素,所以我们 constructor 里面也需要接收一个 element 元素。

/**
 * 监听器
 */
export class Listener 
  constructor(element, recognizer) 

然后我们就可以把我们之前写的所有监听的函数都复制到 Listener 类里面。我们之前是直接调用 startmoveend 事件处理函数的,而这里都换成使用 Recognizer 调用。

都改完之后,我们的 Listener 类应该是这样的。

/**
 * 监听器
 */
export class Listener 
  constructor(element, recognizer) 
    let contexts = new Map();
    let isListeningMouse = false;

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

      recognizer.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);
            recognizer.move(event, context);
          
          button = button << 1;
        
      ;

      let mouseup = event => 
        let context = contexts.get(`mouse$1 << event.button`);
        recognizer.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(touch.identifier, context);
        recognizer.start(touch, context);
      
    );

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

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

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

这样我们就封装好我们的 Listener 监听器了。接下来我们就可以开始封装我们的 Recognizer 识别器。

Recognizer 识别器

Recognizer 是用来封装我们的 start, moveendcancel 四个函数的。这四个函数的作用无非就是识别这些鼠标事件是属于那种类型的,然后把对应的手势类型分发出去。

首先我们的 Recognizer 实例的时候是需要接收一个 Dispatcher 分发器的。这个类会在我们所有事件判断好之后,调用它的 dispatch 分发函数来派发我们的事件。所以我们简单的在 constructor 构造函数中记录下来即可。

然后我们把之前写好的四个函数复制到 Recognizer 。改好后我们整个 Recognizer 就是这样的:

/**
 * 识别器
 */
export class Recognizer 
  constructor(dispatcher) 
    this.dispatcher = dispatcher;
  

  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(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(point, context) 
    context.isFlick = false;

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

    let d, v;
    if (!context.points.length) 
      v = 0;
     else 
      d = Math.sqrt(
      (point.clientX - context.points[0].x) ** 2 + (point.clientY - context.points[0].y) ** 2);
      v = d / (Date.now() - context.points[0].t);
    

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

    if (context.isPan) 
      dispatch('panend', );
    

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

  cancel(point, context) 
    clearTimeout(context.handler);
    console.log('cancel');
  

同学们还记得之前说过,这 4 个事件处理函数中的事件都是还没有分发出去的。所有事件识别后,我们都只是 console.log 打印了一下。

所以接下来我们来完成这一部分的逻辑吧~

首先是 press(或者是 press-start),这个我们是不需要传任何的参数出去的。所以我们直接 disptach 出去即可。

context.handler = setTimeout(() => 
  context.isPan = false;
  context.isTap = false;
  context.isPress = true;
  this.dispatcher.dispatch('press');
  context.handler = null;
, 500);

然后就是 panstart 移动开始这个事件,这个事件就需要把数据传出去的。这里我们就把一下关键数据给分发出去:

  • startX - 开始点的 x 坐标
  • startY - 开始点的 y 坐标
  • clientX - 当前位置的 x 坐标
  • clientY - 当前位置的 y 坐标
  • isVertical - 当前的移动是否是垂直方向的,这个状态在做一些定向功能的时候会有用,所以我们这里附加了这个判断。
    • 计算也很简单,如果 dx 水平线的移动距离小于 dy 垂直的移动距离,那么现在这个移动动作就是垂直的,否则就是水平线的移动。
    • 这里我们要注意的是,要对比的是他们绝对的移动长度,我们是要忽略附属的情况(忽略是往左还是往右,往上还是往下,只需要移动的长度)
    • 所以我们要让 dx 和 dy 是一个正数,这里就使用 Math.abs()

在这个事件里,这 4 个数据就够了,如果遇到一些功能需要 panstart 给予更多的数据,我们可以回到这个库的这里进行添加即可。

if (!context.isPan && dx ** 2 + dy ** 2 > 100) 
  context.isPan = true;
  context.isTap = false;
  context.isPress = false;
  context.isVertical = Math.abs(dx) < Math.abs(dy)
  this.dispatcher.dispatch('panstart', 
    startX: context.startX,
    startY: context.startY,
    clientX: point.clientX,
    clientY: point.clientY,
    isVertical: context.isVertical,
  );
  clearTimeout(context.handler);

接下来的 pan 事件也是与我们的 panstart 的逻辑一样即可。

this.dispatcher.dispatch('pan', 
  startX: context.startX,
  startY: context.startY,
  clientX: point.clientX,
  clientY: point.clientY,
  isVertical: context.isVertical,
);

这里我们改造了一下 panend 触发的位置,因为无论当前是一个移动结束,还是一个 flick,有时候我们功能上是不需要监听 flick 的。所以之前我们存在 flick 就不输出 panend 其实是错误的。

所以这里我们就把 panend 的派发逻辑放在 flick 判断之后,然后放入一个 panend 的派发事件,传出去的参数与上面的 pan 一样,这里加上 isFlick 参数,把当前的移动状态是否是 flick 事件的状态也传出去给手势库的使用者。

虽然我们已经在 panend 中给出了 velocity (速度)的参数。但是使用场景来说,有些时候我们是需要单独监听 flick 事件的。所以我们当前的移动事件是一个 flick 的话,我们也一样会派发一个 flick 事件,并且在传出去的参数中加上 velocity 速度这个参数。

最后 isPan 判断里面的代码是这样的:

let d, v;
if (!context.points.length) 
  v = 0;
 else 
  d = Math.sqrt(
  (point.clientX - context.points[0].x) ** 2 + (point.clientY - context.points[0].y) ** 2);
  v = d / (Date.now() - context.points[0].t);


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


if (context.isPan) 
  this.dispatcher.dispatch('panend', 
    startX: context.startX,
    startY: context.startY,
    clientX: point.clientX,
    clientY: point.clientY,
    isVertical: context.isVertical,
    isFlick: context.isFlick,
  );

好,我们还有一个 press 和 cancel 事件的派发,这里我们就直接使用 this.dispatcher.dispatch 就好了,也不需要传任何的而外参数了。因为这些事件没有必要。

Dispatcher 派发器

最后我们就是来实现我们在 Recognizer 里面用到的 Dispatcher。这个非常的简单,就是把我们 dispatch 函数写入一个 Dispatcher 类里面即可。最后因为 element 是传进来 dipatcher 当中的。所以我们需要在 constructor 里面接收并且记录在类属性当中即可。

/**
 * 分发器
 */
export class Dispatcher 
  constructor(element) 
    this.element = element;
  
  dispatch(type, properties) 
    let event = new Event(type);
    for (let name in properties) 
      event[name] = properties[name];
    
    this.element.dispatchEvent(event);
  

一体化启用函数

最后我们加入一个函数,可以让使用者直接通过这个方法来使用我们的手势库。记住 “高内聚” 的设计理念,就是让你的使用者不需要知道我们封装的服务里的任何复杂内容和使用方式。封装一些简单方便的方法给予使用者们,让他们更友好的使用这个功能。

所以这里我们就加入了 enableGesture 函数,这个函数值需要接收一个 element 参数,即可开启我们所有的事件监听能力。

/**
 * 给某个 element 启用手势库的监听
 * 一体化的处理方法
 *
 * @param Element element 元素
 */
export function enableGesture(element) 
  new Listener(element, new Recognizer(new Dispatcher(element)));

这样我们完美的写好了一个 Gesture 库,它可以完美地给我们的 Carousel 组件提供手势功能了。

接下来我们自己测试一下我们封装的代码是否可靠。在我们的 gesture.html 中引用我们刚刚写的 enable

<body oncontextmenu="event.preventDefault()"></body>
<script>
  import  enableGesture  from './gesture.js';
  enableGesture(document.documentElement);

  document.documentElement.addEventListener('tap', () => 
    console.log('Tapped!');
  );
</script>

没有任何问题的话,当我们点击一下浏览器空白页面的时候,我们 console 中就会输出一个 “Tapped!”。这样就证明我们的手势库可以投入使用了。

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


⭐️ 三哥推荐

开源项目推荐

Hexo Theme Aurora


在最近在版本 1.5.0 更新了以下功能:

预览

✨ 新增

  • 自适应 “推荐文章” 布局 (增加了一个新的 “置顶文章布局” !!)
    • 能够在“推荐文章”和“置顶文章”模式之间自由切换
    • 如果总文章少于 3 篇,将自动切换到“置顶文章”模式
    • 在文章卡上添加了“置顶”和“推荐”标签
    • 📖 文档
  • 增加了与 VuePress 一样的自定义容器 #77
    • Info 容器
    • Warning 容器
    • Danger 容器
    • Detail 容器
    • 查看详情

      用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实现手势库—事件派发与flick事件前端组件化(代码片段)

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

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

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

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

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

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

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

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

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

      手势到文本识别、OCR 或基于手势的本机 .NET 库

      】手势到文本识别、OCR或基于手势的本机.NET库【英文标题】:Gesturetotextrecognition,OCRorGesturebased,native.NETlibrary【发布时间】:2012-02-0306:25:37【问题描述】:我正在为WindowsPhone7开发一个项目,我需要一个用于将形状识别为文本的库... 查看详情

      编写自己的代码库(javascript常用实例的实现与封装)

      编写自己的代码库(javascript常用实例的实现与封装)1.前言大家在开发的时候应该知道,有很多常见的实例操作。比如数组去重,关键词高亮,打乱数组等。这些操作,代码一般不会很多,实现的逻辑也不会很难,下面的代码,... 查看详情

      rn下拉刷新:使用javascript实现(代码片段)

      文章目录效果展示实现步骤UI布局获取偏移量手势处理启用手势移动释放启动和停止下拉刷新问题记录最近一直在做React-Native相关的事情,需要实现一个下拉刷新,Android集成原生很容易,但iOS似乎比较麻烦,于是... 查看详情

      esp8266(esp-12f)第三方库使用--sparkfun_apds9960(手势识别)(代码片段)

      前段时间测试ESP8266+APDS9960做手势识别,利用库函数demo测试方法不对没做成功,换成ArduinoUNO来完成APDS9960的手势识别实验,最近为了用回ESP8266又开始填坑!ESP8266+APDS9960ADPS99603.3RGB红外手势传感器硬件接线APDS9... 查看详情

      esp8266(esp-12f)第三方库使用--sparkfun_apds9960(手势识别)(代码片段)

      前段时间测试ESP8266+APDS9960做手势识别,利用库函数demo测试方法不对没做成功,换成ArduinoUNO来完成APDS9960的手势识别实验,最近为了用回ESP8266又开始填坑!ESP8266+APDS9960ADPS99603.3RGB红外手势传感器硬件接线APDS9... 查看详情

      滚动视图平移防止捏手势?

      ...效果以使用反弹进行平移,但我必须使用捏合手势识别器实现自己的缩放(出于我不会进入的原因).不过,事情是这样的——当我用一根手 查看详情

      超级小的web手势库alloyfinger发布

      针对多点触控设备编程的Web手势组件,快速帮助你的web程序增加手势支持,也不用再担心click300ms的延迟了。拥有两个版本,无依赖的独立版和react版本。除了Dom对象,也可监听Canvas内元素的手势(需要Canvas引擎内置对象支持addEve... 查看详情