问题缘由#
最近使用笔记本的时间多了起来,不得不说,MacBook 触控板确实很舒服,特别是对于一些精细的场景,例如,当我需要将音量从 1% 精准调整到 4% 时,触控板的灵敏度非常合适。然而,问题也随之而来:在很多场景中,当我使用触控板向下滚动时,音量反而升高了。
究其原因,其实并不复杂。使用触控板时,滚动方向遵循的是「自然滚动」原则——向上滑动,内容向下移动;向下滑动,内容向上移动。这种交互逻辑试图模拟现实世界中「推动纸张」的物理直觉。而鼠标滚轮的语义则恰好相反:向上滚动,表示返回到更上方的内容。
正是这种差异,导致同样的滚动方向在不同输入设备上,会产生截然相反的结果。但我们在多数情况下混用鼠标与触控板时,并不会感到困惑,毕竟我们早习惯了这两种逻辑间的切换。
当然,在 macOS 的默认设置中,鼠标与触控板的「自然滚动」只能统一开启或关闭;但对许多并非使用 Magic Mouse 的用户来说,往往会借助诸如 Scroll Reverser 或鼠标厂商自带驱动等第三方工具,实现对鼠标与触控板分别设置滚动方向。事实上,这种方式才更符合传统鼠标的使用逻辑,而 Magic Mouse 在本质上仍更接近于一个触控板。
因此,以下内容将假设鼠标滚轮的滚动方向为「非自然滚动」。
然而,在音量调节时,二者的语义却得到了统一:「向上升高,向下降低」。
当然,上述问题同样建立在「混用鼠标与触控板」这一前提之上。于我而言,在某些特定场景下,确实会如此操作。
如果你并不混用鼠标与触控板,那么事情其实可以变得很简单:在部分视频播放场景中,通过用户脚本直接反转触控板的滚动方向即可(即本文的一个简化场景)。
但对于我,哪怕只是偶尔——我会使用鼠标调节音量大小,我也不希望这个时候鼠标滚轮的行为是不正确的。
那么有没有可能让触控板在调节音量时,也遵循鼠标的逻辑,同时又不影响鼠标的行为呢?
显然,实现一个系统层级的解决方案并不现实。归根结底,我们需要在具体应用的上下文中解决这个问题——毕竟,这种交互需求几乎只会出现在看视频的时候。
为了进一步简化问题,并结合我自己的使用场景,我将目标锁定在 B 站,并决定通过一个网页脚本来实现这一行为的修正,即:实现一个仅针对于 B 站视频内的 Scroll Reverser。
研究思路#
要实现这一点,首先必须区分鼠标和触控板。
遗憾的是,当前浏览器并没有提供任何可以直接区分鼠标和触控板的官方 API。通过大量资料查询,也没有找到完全可靠的解决方案。因此,我们只能依赖一些经验性特征进行推断——大多数现有方法也是如此。
Wheel Event#
在网页中,鼠标滚轮和触控板的滚动行为最终都会被统一抽象为 WheelEvent[1]。
| 参数 | 说明 |
|---|---|
| WheelEvent.deltaX | 返回 double 值,该值表示滚轮的横向滚动量。 |
| WheelEvent.deltaY | 返回 double 值,该值表示滚轮的纵向滚动量。 |
| WheelEvent.deltaZ | 返回 double 值,该值表示滚轮的 z 轴方向上的滚动量。 |
在实际使用中,deltaY 的值会根据不同的输入设备而有所差异。通过观察这些差异,我们或许可以推断出当前的输入设备类型。
尽管,一些方法可以通过分析一段时间内的滚动变化来判断输入设备,准确率也很高。但对于音量调节这种需要即时响应的操作,这种方式显然并不适用。因此,我们需要寻找一种能够在单次滚动事件中区分输入设备的方法。
因此,我写了如下代码方便测试差异。
See the Pen wheel event test by Max C. Foo (@maxchang3) on CodePen.
经过测试,结果如下:
macOS [2] [3]#
1.鼠标滚轮推动下的 deltaY 存在一个最小整数值,除去这个值之外的其他值大概率为浮点数,并且小数点后较为复杂。形如:235.867919921875。
2.在大多数情况下为整数;即使出现浮点数,其形式也相对规整,形如:2.5。
- 经过观察,这个数值会受到网页缩放比例(devicePixelRatio)的影响——原本的整数值在经过缩放系数处理后,可能转化为一个看起来「干净」的浮点数。
Windows [4]#
1.未开启平滑滚动的情况下。通过不同的力度,仅能得到几个整数值。最低为 100。高至 600.
2.开启平滑滚动的情况下,数值为分布在 1 左右的浮点数。
结论#
综合以上测试结果,我们可以构建如下判断策略:
对于 macOS (除 Firefox),可以精准的区分触控板,采取人工矫正的方式,使用鼠标的最低刻度推动获取一个最低的整数 deltaY。除去这个值外的所有数值,进一步判断是否为整数(经过乘 2 处理以避免浮点数 x.5 的情况),即可判断为触控板。
在网页缩放比例为整数倍(如 100%、200%、300%)的情况下,触控板产生的浮点数通常都会呈现为 x.5 的形式,因此乘 2 后判断是否为整数即可。
但是,当网页缩放比例为非整数倍时,这个逻辑显然是是有问题的。为了解决这一问题,我曾尝试过多种方案,例如:
- 根据当前网页缩放比例对 deltaY 进行反向修正
- 判断 deltaY 是否可以被规格化(normalize)为一个整数
- 等等
但这些方法在实际使用中都存在边界情况,始终无法做到完全可靠,只能暂时搁置。
对于 Windows 和任意平台的 Firefox。则需要设定一个合理的阈值,尽管会出现判断出错的情况,但是对于音量精细调节的场景是可以接受的。
以下是一个简单示例,其中 MOUSE_MIN 为鼠标推动下的 deltaY 的最小整数值,-1 为直接取一个固定值 100(也可为其他数值,经测试这个数值相对合理)。
const isTrackpad = (wheelEvent: WheelEvent) => { if (MOUSE_MIN === -1) return Math.abs(wheelEvent.deltaY) < 100 return Math.abs(wheelEvent.deltaY) != MOUSE_MIN && Number.isInteger(wheelEvent.deltaY * 2)}最终成果#
至此,输入设备的判断逻辑已经基本确定。
以上判断逻辑确定后,接下来就是对 B 站的视频音量控制进行拦截和处理了。
我通过 Hook EventTarget.prototype.addEventListener 拦截对应的 mousewheel 事件。(所以 b 站为什么不用 wheel!)
判断是否为触控板,添加代理拦截 wheelDelta 值,取相反数(这里直接取 deltaY 后做一定计算处理,他与 wheelDelta 正负相异)后返回。
最后,我实现了一个油猴脚本,使用 vite-plugin-monkey 进行工程化开发!完整项目请看这里~
如果你感兴趣,可以安装使用:
[Greasyfork] [Github Release] [Github Pages]
一点展望#
需要强调的是,这一方案本质上仍然是观察得到的特征进行的经验判断,而非对浏览器内部实现机制的分析或利用。这也正是我在最后想进一步讨论的问题。
之前我产生过一个大胆的想法:既然 macOS 系统层级有丰富的触控板事件接口,如果 Windows 也能对等支持,我们是否可以维护一个本地中转服务,将最原始的鼠标/触控板数据实时推送到网页端(例如通过浏览器插件实时通讯?)虽然实现代价确实有点大。
希望有相关的标准早点进浏览器吧(也许我以后会去提一个 RFC?)
注释#
[1] 来自 MDN,有省略。B 站实际使用的是 mousewheel event。由于该标准已经不推荐使用,所以这里使用 WheelEvent。
[2] 仅 Chrome / Safari,Firefox 下全部为整数值。