Mac 鼠标滚轮一格一格地跳
macOS 把鼠标滚轮当作一串离散的「格」交给应用,而触控板拿到的是连续手势。要让滚轮变顺滑,就得把这些格拦截下来、按帧重新投递——三个真实的坑就藏在这里。
macOS 拿到滚轮后到底做了什么
触控板上报的是连续手势——系统收到一串位置,并且知道手指还在动。鼠标滚轮上报的是离散的「格」:一格就是一个事件,附带一个行数,macOS 把它交给应用,然后就不管了。
所以滚轮「一格一格地跳」,不是哪里坏了,而是它本来就被这样要求:一格,一跳。会平滑滚轮的应用程序是自己做的;大多数不做。
真正有效的机制
三步,缺一不可:
- 在应用看到之前拦截。用
CGEventTap先拿到滚轮事件,这样才能把它吞掉并替换。 - 累积,而不是一次性消费。引擎保留这一格还没用掉的余量,按帧一点点发出去。
- 跟着显示器的时钟发。用
CVDisplayLink与面板刷新同步,增量就跟着帧到达,而不是成串地冲出去。
这正是开源项目 MOS 普及开来的架构,也是正确的做法——我们的滚动引擎是照它实现的,而不是另发明一个更差的滚轮。
坑一——并不是每一格都意味着「滚动」
这一条最咬人。在「照片」「地图」「Keynote」「Numbers」以及大多数表格里,一格滚轮根本不是滚动,而是缩放的一步或翻一页。
一旦平滑化,这一格会在你松手很久之后还在到达:照片从 80% 跳到 200%,幻灯一次翻过十几页。用户于是用「按应用加白名单」来绕开——而这恰恰是这类功能本该替人省掉的手工活。
所以在改造一个事件之前,引擎先问一个问题:这是滚动还是缩放?缩放语义的事件原样放行,不做任何加工。对滚轮本来就是「按行精确」的应用同理——再加工只会更差,不会更好。
坑二——悄悄变成僵尸的显示时钟
CVDisplayLink 并不总能活过一夜。显示器唤醒之后,或者发生重配置(换线、过 KVM、改分辨率),它可能停止投递回调,却仍然声称自己在运行。
如果没人检查,外面看到的现象就是滚动突然不动画了——以及「这应用莫名其妙坏了」。修法并不漂亮:假定这个时钟会死,检测它,然后重建它。
坑三——两个工具抢同一个滚轮
同一层上的两个事件监听器都会消费滚轮。每个引擎都把对方吞掉的事件当成「输入丢了」,又把自己重新发出的事件当成「新输入」。两个插值器于是互相追,结果就是滚动卡顿、翻倍,或者干脆停摆。
用户很自然地得出结论:新装的那个工具搞坏了我的滚动。把冲突说出来(「检测到另一个滚动工具在运行」)远比让人盲目排查有用。
如果现在滚动就很难受,先查这几条
- 是不是有第二个平滑工具在跑?两个同时跑比一个都不跑更糟。
- 那个应用用的是不是按行精确的增量?对它们再加工只会白白增加延迟。
- 是不是睡眠、唤醒或换线之后才开始的?那指向显示时钟,不指向应用。
- 是某一个应用,还是所有应用?只有一个通常是语义不匹配(坑一);所有应用都这样,问题在监听器本身。
诚实的边界
平滑是改造输入,不是创造输入。它天然会带来一点延迟,也无法让一个本来就忽略精确增量的应用变好。当滚轮本来就已经正常时,正确答案是别去动它——这也正是上面那些语义与「按行精确」判断存在的原因。
相关
按时段调暗,最后把屏幕调成了全黑 · 外接显示器的亮度怎么调? · 鼠标加速关不掉 · 全部指南 · DDC/CI 无响应 · 休眠后亮度被重置 · 文字发虚 · 色彩发灰 · 该用哪个工具?