最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 用 JavaScript 实现手势库 — 事件派发与 Flick 事件【前端组件化】

    正文概述 掘金(三钻)   2021-05-22   519

    前端《组件化系列》目录

    • 「一」用 JSX 建立组件 Parser(解析器)
    • 「二」使用 JSX 建立 Markup 组件风格
    • 「三」用 JSX 实现 Carousel 轮播组件
    • 「四」用 JavaScript 实现时间轴与动画
    • 「五」用 JavaScript 实现三次贝塞尔动画库 - 前端组件化
    • 「六」用 JavaScript 实现手势库 - 实现监听逻辑
    • 「七」用 JavaScript 实现手势库 — 手势逻辑
    • 「八」用 JavaScript 实现手势库 — 支持多键触发
    • 「九」用 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' 消息了!

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

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

    实现一个 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 毫秒内的点来计算速度即可,其余的点就可以过滤掉了。

    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;
        console.log('press-start');
        context.handler = null;
      }, 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,
      });
    };
    
    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, 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;
          dispatch('panend', {});
        }
      }
    
      if (context.isPress) {
        console.log('press-end');
      }
    };
    
    let cancel = (point, context) => {
      clearTimeout(context.handler);
      console.log('cancel');
    };
    
    function dispatch(type, properties) {
      let event = new Event(type);
      for (let name in properties) {
        event[name] = properties[name];
      }
      element.dispatchEvent(event);
    }
    

    下一期,我们就来做手势库的最后一步,封装!~


    ⭐️ 三哥推荐

    开源项目推荐

    Hexo Theme Aurora

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

    :sparkles: 新增

    • 自适应 “推荐文章” 布局 (增加了一个新的 “置顶文章布局” !!)
      • 能够在“推荐文章”和“置顶文章”模式之间自由切换
      • 如果总文章少于 3 篇,将自动切换到“置顶文章”模式
      • 在文章卡上添加了“置顶”和“推荐”标签
      • :book: 文档
    • 增加了与 VuePress 一样的自定义容器 #77
      • Info 容器
      • Warning 容器
      • Danger 容器
      • Detail 容器
      • 预览
    • 支持了更多的 SEO meta 数据 #76
      • 添加了 description
      • 添加了 keywords
      • 添加了 author
      • :book: 文档

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

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

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

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


    VSCode Aurora Future

    用 JavaScript 实现手势库 — 事件派发与 Flick 事件【前端组件化】 对,博主还做了一个 Aurora 的 VSCode 主题。用了Hexo Theme Aurora 相对应的颜色色系。这个主题的重点特性的就只用了 3 个颜色,减少在写代码的时候被多色多彩的颜色所转移了你的注意力,让你更集中在写代码之中。

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


    Firefox Aurora Future

    用 JavaScript 实现手势库 — 事件派发与 Flick 事件【前端组件化】我不知道大家,但是最近我在用火狐浏览器来做开发了。个人觉得火狐还真的是不错的。推荐大家尝试一下。

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


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

    博主开始在B站直播学习,欢迎过来《直播间》一起学习。

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

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


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


    起源地下载网 » 用 JavaScript 实现手势库 — 事件派发与 Flick 事件【前端组件化】

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元