灏天阁

一文讲透鼠标及触控板开发、使用技巧

· Yin灏

[TOC]

开发白板过程中,画布平移缩放操作是使用很频繁且最基础的交互功能。由于需求排期一直未实现该功能,导致白板开发使用过程中体验差点意思,有一天我终于忍不了了,花了一个晚上研究了一下相关的内容。所以有了以下实践。

一、在白板类绘图产品中,平移缩放大部分都是依靠鼠标/触控板操作完成的。这里不可避免的要处理鼠标事件相关的代码逻辑。针对不同设备需要兼容处理不同的逻辑,尤其在使用 mac 触控板时左滑返回上一页的操作很干扰画图,会导致浏览器页面后退,需要禁用该操作。

二、在我们日常工作生活中,几乎每天都需要用鼠标或者触控板。但是发现周围大部分用 mac 的同事使用触控板的方式都不太高效,还是使用普通鼠标那一套操作。不愿更新自己的使用习惯。

针对以上原因,我要提问了:我们真的会运用鼠标和触控板吗?

这个问题包含两层含义:

  1. 作为 mac 用户:触控板我们使用习惯是否高效。
  2. 作为前端开发者:触控板相关的需求我们能否实现。

接下来我们带着问题来寻找答案。

TL;DR

看好多国外的文章,太长了都会有 TL;DR(Too Long; Didn’t Read.),我认为那不是 Too Long;Didn’t Read,而是 Too Lazy; Didn’t Read. 😂 所以我也先把结论写在这里。

通过本文能学到的知识:

  1. 鼠标模式
    1. 鼠标滚轮缩放画布
    2. 右键长按拖拽平移画布
  2. 触控板模式
    1. 双指移动平移画布
    2. 双指捏合缩放画布
    3. 禁用双指轻扫在页面间切换
    4. 禁用智能缩放
  3. 触控板的高级用法

下面开始正文:

对 mac 操作很溜的选手可以忽略“触控板使用”这节。 但是我发现身边大部分使用 mac 的同事都不怎么会熟练使用触控板的高级功能,还仅仅停留在能用的阶段。我个人推荐你阅读下升级自己的使用习惯。

-–»> 忽略开始 «<—

1 基础概念

1.1 鼠标、触控板

如下图所示分别是:Magic Trackpad 妙控板(文中统称触控板)、Magic Mouse 妙控鼠标、 普通鼠标。

(Magic Trackpad 妙控板 / Magic Mouse 妙控鼠标 / 普通鼠标)

本节所说的区别特指Apple 触控设备普通鼠标的交互区别。因为妙控板和妙控鼠标交互形式是一致的,都是单指、双指点击或者滑动以及多指滑动,而普通鼠标则是按键点击与滚轮滚动与 Apple 触控设备有着明显的区别。

1.2 点击和手势

相信现在工作的大家对鼠标的的操作习惯,都来源于 Windows 时代的人机交互。

而 Apple 设备改进了这一交互习惯。主要还是得益于硬件设备支持了多点触控。有了多点触控就可以增加更多的手势交互。当年在鼠标时代就用过很多浏览器插件,支持鼠标手势,通过鼠标画一个预先设置好的图形,就可以执行某些具体的操作,但是用鼠标画图还是有些难以操作。

我们直接来看 Apple 的手势操作。

触控板常用到的手势操作有:

滚动缩放类:

  • 放大缩小
  • 智能缩放

这里提出一个问题:明明是缩放,为什么他们的设置项要叫滚动缩放?

更多手势类:

  • 常规点击操作
  • 在页面之间轻扫
  • 在全屏幕 App 之间轻扫
  • 调度中心
  • App Exposé
  • 启动台
  • 显示桌面

2 触控板独有功能以及推荐用法

这点是我要重点推荐的,观察到大家平时操作触控板拖拽窗口,或者拖拽文件基本都是以下步骤:

  1. 触控板移动光标到指定位置;
  2. 单指使劲按下触控板;
  3. 开始拖动文件,此时还的一直使劲按着触控板;
  4. 拖到指定位置松手。

到第 2、3 步的时候体验太糟糕了。手指既要向下用力,又要向左右用力滑动,一不小心松劲了,拖着的文件又跑回原来的地方了,或者压根拖到了不该去的地方。真是难受。

使用了下面推荐的方法之后,你就可以随意拖动文件,而且毫不费力,

2.1 触控板三指拖移

设置方法如下:

  • 设置 > 辅助功能 > 指针控制 > 鼠标与触控板 > 触控板选项 (触控板三指拖移设置)
  • 如果懒得找那么深的设置,也可以直接搜索:“拖移”。 可以看到 Apple 给他的定位描述 “让鼠标和触控板更易于使用

提醒:由于三指被占用,原来触控板里的“更多手势”里凡是用到三指的都要改成四指,避免冲突。 接下来几天你的操作可能会特别难受,但是习惯的升级总是要经历一个过程的。

以上就是触控板的最便于使用的方法。

-–»> 忽略结束 «<—

接下来就是代码部分了。

-————————-我是分割线————————–

3 鼠标触控板开发

文章开头提出的第二个问题:我们要实现触控板/鼠标的平移缩放功能该如何实现呢?相信大部分前端开发者第一反应想到监听鼠标滚轮事件。没错,就是运用鼠标滚轮事件。

此时我又要提问了:

  • 触控板的双指捏合缩放页面怎么实现呢?
  • 触控板的双指任意方向滑动平移页面该怎么实现呢?
  • 触控板又该如何禁用双指轻扫在页面间切换?

这些如果你还不太清楚,那就的接着往下看了。

下面我们继续带着问题来找答案。以下是我找答案的过程:

  1. 本着面向 Google 编程的思想,在最开始我先 Google 了一通 “触控板 禁用 双指 后退 返回 trackpad disable“ 等等关键词,搜索结果都是移动端页面触控手势,触控板的文章少得可怜,基本都是介绍触控板的使用,代码相关的一无所获。
  2. 后来我在想会不会触控板有我不知道的双指事件,我又去 developer.apple.com 一通搜索也是只有 Mouse and Trackpad 的功能介绍,在深入就到 Appkit 了显然这条路也不对。
  3. 一筹莫展之下 stack overflow 永远不会让你失望。在这里找到了一个问题: Detect touchpad vs mouse in Javascript ,心想反正也要判断触控板和鼠标,那就抄一下这个答案吧。抄答案时候发现基本用到的都是 mousewheel 事件。
  4. 所以 mdn 接着找 mousewheel 资料,发现 mousewheel: This feature is no longer recommended. 推荐用 wheel,似乎我们已经找到了解决问题的方法。

用到的对象主要是 WheelEvent。除他之外还有几个关联的事件对象要熟悉:MouseEventUIEventEvent。这些事件对象都是依次继承下来的。

到这里答案基本出来了解决上面的问题主要运用的就是:wheel 鼠标滚轮事件。到这里也回答了上面的问题:为何双指缩放等设置项会归类到滚动缩放下,因为他们的实现都是使用了 wheel事件

那我们就细细研究一下 wheel事件,先看他的几个对我们来说有用的属性:

  • WheelEvent.deltaX 只读:返回 double 值,该值表示滚轮的横向滚动量。
  • WheelEvent.deltaY 只读:返回 double 值,该值表示滚轮的纵向滚动量。

从事件属性可以看出该事件主要用来监听鼠标滚轮的滚动,并可以拿到滚动量。 为了搞的更清楚这几个属性所代表的意义,接下来我做了个小实验,来探究 delta 的变化规律。

3.1 wheel 小实验

实验说明: 页面有两个 div,内部 div 比外部的大,通过监听滚轮事件滚动寻找 deltaX、deltaY 的规律。 同时将 scrollTop、scrollLeft 打印出来进行参照看 div 的滚动方向。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
    />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>你不知道的 Trackpad</title>
    <style>
      body {
        background-color: #25252580;
        overflow: hidden;
      }
      #root {
        position: fixed;
        top: calc(50% - 150px);
        left: calc(50% - 150px);

        width: 300px;
        height: 300px;

        /* 任意方向滚动 */
        overflow: scroll;

        /* 纵向滚动效果 */
        /*overflow-x: hidden;*/
        /*overflow-y: scroll;*/

        /* 横向滚动效果 */
        /*overflow-x: scroll;*/
        /*overflow-y: hidden;*/
      }
      h1 {
        position: absolute;
        top: 0;
        left: 0;
      }
      #container {
        width: 1000px;
        height: 1000px;
        /* 背景色渐变 */
        background: linear-gradient(
            217deg,
            rgba(255, 0, 0, 0.8),
            rgba(255, 0, 0, 0) 70.71%
          ), linear-gradient(
            127deg,
            rgba(0, 255, 0, 0.8),
            rgba(0, 255, 0, 0) 70.71%
          ), linear-gradient(336deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 70.71%);
      }
    </style>
  </head>
  <body>
    灰色的都是body
    <div id="root">
      <h1>你不知道的 Trackpad</h1>
      <div id="container"></div>
    </div>

    <script>
      document.addEventListener(
        "wheel",
        function (e) {
          e.stopPropagation();
          e.preventDefault();
          return false;
        },
        { passive: false }
      );
      document.getElementById("root").addEventListener(
        "wheel",
        function (e) {
          e.stopPropagation();
          logInfo(e, "root");
          isTrackpadOrMouse(e);
        },
        { passive: false }
      );

      let hnum = 1;
      let vnum = 1;
      // 临时存放日志,方便以表格展示方便对比结果
      let temp = [];
      // 是否一直打印日志
      let always = false;

      /**
       * 重点看这几个属性:deltaX、deltaY、ctrlKey
       */
      function logInfo(e, name) {
        let info = {
          name: name,
          button: e.button,
          ctrlKey: e.ctrlKey,
          // altKey: e.altKey,
          // metaKey: e.metaKey,
          shiftKey: e.shiftKey,
          // which: e.which,
          // clientX: e.clientX,
          // clientY: e.clientY,
          deltaMode: e.deltaMode,
          deltaX: e.deltaX,
          deltaY: e.deltaY,
          // deltaZ: e.deltaZ,
          // layerX: e.layerX,
          // layerY: e.layerY,
          // offsetX: e.offsetX,
          // offsetY: e.offsetY,
          // wheelDelta: e.wheelDelta,
          wheelDeltaX: e.wheelDeltaX,
          wheelDeltaY: e.wheelDeltaY,
          scrollTop: document.getElementById(name).scrollTop,
          scrollLeft: document.getElementById(name).scrollLeft,
          // x: e.x,
          // y: e.y,
        };
        if (always || hnum % 10 === 0 || vnum % 10 === 0) {
          temp.push(info);
        }

        // 垂直滚动时
        if (e.deltaY > 0) {
          vnum += 1;
        } else if (e.deltaY < 0) {
          vnum -= 1;
        }

        // 水平滚动时
        if (e.deltaX > 0) {
          hnum += 1;
        } else if (e.deltaX < 0) {
          hnum -= 1;
        }

        if (always || hnum === 0 || vnum === 0) {
          console.table(always ? [info] : temp);
        }
      }

      /**
       * 判断是鼠标或者触控板
       * @param e
       * @return {boolean}
       */
      function isTrackpadOrMouse(e) {
        let isTrackpad = false;
        if (e.wheelDeltaY) {
          if (e.wheelDeltaY === e.deltaY * -3) {
            isTrackpad = true;
          }
        } else if (e.deltaMode === 0) {
          isTrackpad = true;
        }

        console.log(isTrackpad ? "Trackpad detected" : "Mousewheel detected");
        return isTrackpad;
      }
    </script>
  </body>
</html>

实验效果图:

3.2 实验结论

通过实验发现:

3.2.1 触控板

  • 平移 是判断 deltaXdeltaY 正负
  1. deltaX 连续为正数:向左 👈
  2. deltaX 连续为负数:向右 👉
  3. deltaY 连续为正数:向上 👆
  4. deltaY 连续为负数:向下 👇

为什么要强调连续,因为触控板比较灵敏(灵敏度可调),而且滚动会有惯性(惯性也可设置)。偶尔一次正负变化不能说明确切方向,必须连续才能说明方向变化。

灵敏度、惯性设置还是在辅助功能这里:

  • 缩放 是判断 deltaY + ctrlKey 的值
  1. ctrlKey = true & deltaY 连续为正数:向内
  2. ctrlKey = true & deltaY 连续为负数:向外

3.2.2 鼠标

普通鼠标滚轮一般只有上下两个方向,所以鼠标模式下:

  • 缩放 是滚轮滚动
  • 平移 是长按右键拖拽

我的实验代码为了简单没有做连续差值计算,只是简单判断了正负,连续的更加严谨。

3.3 禁用触控板双指轻扫返回、智能缩放

平移缩放功能实现了,还差怎么禁用智能缩放,和清扫返回上一页。 下图这个箭头你一定见过,平时用着挺方便,但是在画图软件里,想平移画布结果页面跳转了,太影响操作。所以需要掉它。

而智能缩放更像是一个放大镜,他与浏览器的缩放还不一样。 浏览器的缩放是将页面的每个元素都等比例放大。 智能缩放的放大就像缩放图片一样,页面超出浏览器窗口的元素都看不见了无法操作。 所以也需要禁掉智能缩放。

在操作部分我们说过,双指轻扫返回、智能缩放都归类在滚动里,并且上面实验也证实他也是使用了 wheel 事件只不过事件绑定在了 document 上。所以我们只需要禁止 document 上的 wheel 事件默认行为就可以了禁止这些操作。

  • addEventListenerpassive 属性

看 MDN 文档说明:

passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

addEventListenerpassive 默认是 true,只有 passive:falsee.preventDefault() 才会生效。

document 拦截了滚轮事件默认行为后,因为该事件会传播冒泡,所以需要注意页面上其他的子元素,如果有滚动条之类的也会失效,需要在子元素上阻止冒泡。

document.addEventListener(
  "wheel",
  function (e) {
    e.stopPropagation();
    // 所以实际代码中要在这里判断,只有某些情况才禁用默认行为。
    e.preventDefault();
    return false;
  },
  { passive: false }
);

3.4 注意点

注意事项: 请勿想当然依据滚轮方向(即该事件的各 delta 属性值)来推断文档内容的滚动方向,因标准未定义滚轮事件具体会引发什么样的行为,引发的行为实际上是各浏览器自行定义的。即便滚轮事件引发了文档内容的滚动行为,也不表示滚轮方向和文档内容的滚动方向一定相同。因而通过该滚轮事件获知文档内容滚动方向的方法并不可靠。要获取文档内容的滚动方向,可在文档内容滚动事件(scroll (en-US))中监听scrollLeftscrollTop二值变化情况,即可推断出滚动方向了。

详细的属性说明看 MDN wheel 事件文档 ,不在这里粘文档了。

到这里基本上该有的功能就都实现了。本文实现需求在文章开头已经给出这里不再赘述。 这篇文章到此也就结束了。希望会对你有所帮助。同时感谢以下博客主为我们做出的探索与尝试。

参考连接

  1. https://juejin.cn/post/6844904023598825486
  2. https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript
  3. https://support.apple.com/en-ug/HT204895
  4. https://www.apude.com/blog/5979.html

- Book Lists -