在各种浏览器中统一鼠标滚轮速度

161

针对另一个问题,我写了这个答案,其中包括了这份示例代码

在那份代码中,我使用鼠标滚轮来缩放HTML5画布。我找到了一些可以规范Chrome和Firefox之间速度差异的代码。然而,在Safari中,缩放处理的速度比这两者都快得多。

以下是我目前拥有的代码:

var handleScroll = function(e){
  var delta = e.wheelDelta ? e.wheelDelta/40 : e.detail ? -e.detail/3 : 0;
  if (delta) ...
  return e.preventDefault() && false;
};
canvas.addEventListener('DOMMouseScroll',handleScroll,false); // For Firefox
canvas.addEventListener('mousewheel',handleScroll,false);     // Everyone else

如何编写代码以在Chrome v10/11、Firefox v4、Safari v5、Opera v11和IE9上滚动相同距离的鼠标滚轮事件?

此问题相关,但没有好的答案。

编辑:进一步调查显示,一个向上的滚动事件为:

                  | evt.wheelDelta | evt.detail
------------------+----------------+------------
  Safari v5/Win7  |       120      |      0
  Safari v5/OS X  |       120      |      0
  Safari v7/OS X  |        12      |      0
 Chrome v11/Win7  |       120      |      0
 Chrome v37/Win7  |       120      |      0
 Chrome v11/OS X  |         3 (!)  |      0      (possibly wrong)
 Chrome v37/OS X  |       120      |      0
        IE9/Win7  |       120      |  undefined
  Opera v11/OS X  |        40      |     -1
  Opera v24/OS X  |       120      |      0
  Opera v11/Win7  |       120      |     -3
 Firefox v4/Win7  |    undefined   |     -3
 Firefox v4/OS X  |    undefined   |     -1
Firefox v30/OS X  |    undefined   |     -1

此外,在OS X上使用MacBook触摸板即使移动缓慢也会产生不同的结果:

  • 在 Safari 和 Chrome 上,鼠标滚轮的 wheelDelta 值为 3 而非 120。
  • 在 Firefox 中,detail 通常为 2,有时为 1,但当滚动非常缓慢时没有任何事件处理程序被触发

因此问题是:

最好的方法是什么,可以区分这种行为(理想情况下不需要任何用户代理或操作系统嗅探)?


抱歉,我删除了我的问题。我正在撰写答案。在我进一步深入之前,你是在谈论 Mac OS X 上的 Safari 滚动吗?当你滚动一点时,它会滚动一点,但如果你保持一个恒定的速率,它会逐渐加速吗? - Blender
@Blender 我现在正在测试OS X,是的,Safari是一个异常值,它的缩放速度比Chrome快大约20倍。不幸的是,我没有连接物理鼠标,所以我的测试受到了限制,只能使用≈相当距离和速度的两个手指滑动。 - Phrogz
我已经更新了问题,详细说明了在OS X和Win7上排名前5的浏览器的行为。这是一个雷区,Chrome在OS X上似乎是有问题的异常值。 - Phrogz
网页应该有一个关于滚动/滚轮事件触发频率和时间的网络标准。 - www139
https://developer.mozilla.org/zh-CN/docs/Web/API/WheelEvent/deltaMode 是关键,参见 @George 的回答。 - Charles L.
显示剩余4条评论
10个回答

59

2014年9月编辑

鉴于以下原因:

  • 过去和将来,同一浏览器在OS X上的不同版本可能产生不同的值,并且
  • 在OS X上使用触摸板与使用鼠标滚轮产生非常相似的效果,但会提供非常不同的事件值,但设备差异无法被JS检测到。

... 我只能建议使用这个简单的基于符号计数的代码:

var handleScroll = function(evt){
  if (!evt) evt = event;
  var direction = (evt.detail<0 || evt.wheelDelta>0) ? 1 : -1;
  // Use the value as you will
};
someEl.addEventListener('DOMMouseScroll',handleScroll,false); // for Firefox
someEl.addEventListener('mousewheel',    handleScroll,false); // for everyone else

以下是我最初试图编写的规范化数值的脚本。但在 Mac OS X 上有两个问题:使用 Firefox 的值会比应该的值小三分之一,而使用 Chrome 的值会比应该的值小四十分之一。

// Returns +1 for a single wheel roll 'up', -1 for a single roll 'down'
var wheelDistance = function(evt){
  if (!evt) evt = event;
  var w=evt.wheelDelta, d=evt.detail;
  if (d){
    if (w) return w/d/40*d>0?1:-1; // Opera
    else return -d/3;              // Firefox;         TODO: do not /3 for OS X
  } else return w/120;             // IE/Safari/Chrome TODO: /3 for Chrome OS X
};

您可以在此处使用自己的浏览器测试此代码:http://phrogz.net/JS/wheeldelta.html

欢迎提供有关在Mac OS X上检测和改进Firefox和Chrome行为的建议。

编辑: @Tom提出的一个建议是将每个事件调用视为单个移动,并使用距离的符号进行调整。这不会在OS X上的平滑/加速滚动下产生很好的结果,也不能完美地处理鼠标滚轮非常快速移动(例如wheelDelta为240)的情况,但这些情况并不经常发生。由于文中所述的原因,此代码现在是此答案顶部显示的推荐技术。


@Phrogz,你在2014年9月是否有更新版本,包括所有的OS X / 3?这将是社区的一个很好的补充! - Basj
@Phrogz,这太棒了。我这里没有Mac来测试...(即使我自己声望不高,我也很乐意为此提供赏金;) - Basj
@Basj 我已经在原问题中更新了测试结果。鉴于(a)OS X上的Safari现在产生的结果是12而不是120,而所有其他OS X上的浏览器都变得更好了,以及(b)无法区分来自触摸板和鼠标的事件,但它们提供截然不同的事件细节,因此我正在编辑我的答案,建议使用简单的wheelDirection测试。 - Phrogz
1
在Windows Firefox 35.0.1上,wheelDelta未定义,detail始终为0,这使得提供的代码失败。 - Max Strater
1
@MaxStrater 遇到了同样的问题,我已经添加了“deltaY”来克服这个方向上的问题,例如(((evt.deltaY <0 || evt.wheelDelta>0) || evt.deltaY < 0) ? 1 : -1)不确定QA会发现什么问题。 - Brock
显示剩余6条评论

37

我们的 Facebook 朋友为此问题提供了一种绝佳的解决方案。

我已经在使用 React 构建的数据表上进行了测试,它滚动得非常流畅!

这个解决方案适用于各种浏览器,在 Windows/Mac 上以及使用触控板/鼠标时都可行。

// Reasonable defaults
var PIXEL_STEP  = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;

function normalizeWheel(/*object*/ event) /*object*/ {
  var sX = 0, sY = 0,       // spinX, spinY
      pX = 0, pY = 0;       // pixelX, pixelY

  // Legacy
  if ('detail'      in event) { sY = event.detail; }
  if ('wheelDelta'  in event) { sY = -event.wheelDelta / 120; }
  if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
  if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }

  // side scrolling on FF with DOMMouseScroll
  if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
    sX = sY;
    sY = 0;
  }

  pX = sX * PIXEL_STEP;
  pY = sY * PIXEL_STEP;

  if ('deltaY' in event) { pY = event.deltaY; }
  if ('deltaX' in event) { pX = event.deltaX; }

  if ((pX || pY) && event.deltaMode) {
    if (event.deltaMode == 1) {          // delta in LINE units
      pX *= LINE_HEIGHT;
      pY *= LINE_HEIGHT;
    } else {                             // delta in PAGE units
      pX *= PAGE_HEIGHT;
      pY *= PAGE_HEIGHT;
    }
  }

  // Fall-back if spin cannot be determined
  if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
  if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }

  return { spinX  : sX,
           spinY  : sY,
           pixelX : pX,
           pixelY : pY };
}

源代码可在此找到:https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js


3
这是需要翻译的内容:一个更直接的链接,未将其捆绑到normalizeWheel.js的原始代码 https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js。这是翻译后的结果:提供一个未捆绑至normalizeWheel.js原始代码的更直接链接:https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js。 - Robin Luiten
这个东西真是太棒了。刚刚利用它,就像魔法一样好用!干得好,Facebook :) - perry
你能给一些如何使用它的例子吗?我尝试了在火狐浏览器中可以工作,但在Chrome或IE(11)中不行。谢谢。 - Andrew
5
对于使用 npm 的任何人而言,已从 Facebook 的 Fixed Data Table 中提取了此代码,并准备好供使用。有关更多详细信息,请参见此处 https://www.npmjs.com/package/normalize-wheel。 - Simon Watson
2
我使用这个代码来控制缩放,但对我来说,在使用Macbook触摸板和我的Logitech MX Anywhere 2鼠标时,这段代码有很大的区别。 - Dirk Boer
显示剩余3条评论

28

这是我疯狂尝试制作一个跨浏览器的一致性和标准化的增量 (-1 <= delta <= 1):

var o = e.originalEvent,
    d = o.detail, w = o.wheelDelta,
    n = 225, n1 = n-1;

// Normalize delta
d = d ? w && (f = w/d) ? d/f : -d/1.35 : w/120;
// Quadratic scale if |d| > 1
d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n;
// Delta *should* not be greater than 2...
e.delta = Math.min(Math.max(d / 2, -1), 1);

这完全是经验性的,但在Safari 6、FF 16、Opera 12(OS X)和IE 7上运行得非常好(XP)。


3
如果我能再点赞10次,我一定会的。非常感谢你! - pyronaur
请问你能否在演示中提供完整的功能代码(例如 jsFiddle)? - adardesign
o 中缓存 event 对象有什么原因吗? - yckart
没有。o变量的存在是为了表明我们想要原始事件,而不是像jQuery或其他库可能传递给事件处理程序的包装事件。 - smrtl
@smrtl,你能解释一下n和n1吗?这些变量有什么作用? - Om3ga
1
我认为你也应该在var中声明f - Bernát

12

我制作了一个表格,列出了不同事件/浏览器返回的不同值,考虑到DOM3已经被一些浏览器支持的wheel事件(表格如下)。

基于此,我编写了这个函数来规范化速度:

http://jsfiddle.net/mfe8J/1/

function normalizeWheelSpeed(event) {
    var normalized;
    if (event.wheelDelta) {
        normalized = (event.wheelDelta % 120 - 0) == -0 ? event.wheelDelta / 120 : event.wheelDelta / 12;
    } else {
        var rawAmmount = event.deltaY ? event.deltaY : event.detail;
        normalized = -(rawAmmount % 3 ? rawAmmount * 10 : rawAmmount / 3);
    }
    return normalized;
}

鼠标滚轮(mousewheel)、滚轮(wheel)和DOMMouseScroll事件的表格:

| mousewheel        | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 9 & 10   | IE 7 & 8  |
|-------------------|--------------|--------------|---------------|---------------|----------------|----------------|----------------|-----------|-------------|-----------|
| event.detail      | 0            | 0            | -             | -             | 0              | 0              | 0              | 0         | 0           | undefined |
| event.wheelDelta  | 120          | 120          | -             | -             | 12             | 120            | 120            | 120       | 120         | 120       |
| event.wheelDeltaY | 120          | 120          | -             | -             | 12             | 120            | 120            | undefined | undefined   | undefined |
| event.wheelDeltaX | 0            | 0            | -             | -             | 0              | 0              | 0              | undefined | undefined   | undefined |
| event.delta       | undefined    | undefined    | -             | -             | undefined      | undefined      | undefined      | undefined | undefined   | undefined |
| event.deltaY      | -100         | -4           | -             | -             | undefined      | -4             | -100           | undefined | undefined   | undefined |
| event.deltaX      | 0            | 0            | -             | -             | undefined      | 0              | 0              | undefined | undefined   | undefined |
|                   |              |              |               |               |                |                |                |           |             |           |
| wheel             | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 10 & 9   | IE 7 & 8  |
| event.detail      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
| event.wheelDelta  | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaY | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaX | 0            | 0            | undefined     | undefined     | -              | 0              | 0              | undefined | undefined   | -         |
| event.delta       | undefined    | undefined    | undefined     | undefined     | -              | undefined      | undefined      | undefined | undefined   | -         |
| event.deltaY      | -100         | -4           | -3            | -0,1          | -              | -4             | -100           | -99,56    | -68,4 | -53 | -         |
| event.deltaX      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
|                   |              |              |               |               |                |                |                |           |             |           |
|                   |              |              |               |               |                |                |                |           |             |           |
| DOMMouseScroll    |              |              | Firefox (win) | Firefox (mac) |                |                |                |           |             |           |
| event.detail      |              |              | -3            | -1            |                |                |                |           |             |           |
| event.wheelDelta  |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaY |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaX |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.delta       |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaY      |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaX      |              |              | undefined     | undefined     |                |                |                |           |             |           |

2
在macOS下,当前Safari和Firefox的滚动速度不同。 - Lenar Hoyt

6

还有一种更或多或少独立的解决方案......

不过这种方法并没有考虑事件之间的时间。有些浏览器似乎总是以相同的增量触发事件,并且在快速滚动时只是更快地触发它们。而其他浏览器则会变化增量。可以想象一个自适应的标准化程序,它考虑到时间,但是这会变得有些复杂和难用。

可在此处查看工作结果:jsbin/iqafek/2

var normalizeWheelDelta = function() {
  // Keep a distribution of observed values, and scale by the
  // 33rd percentile.
  var distribution = [], done = null, scale = 30;
  return function(n) {
    // Zeroes don't count.
    if (n == 0) return n;
    // After 500 samples, we stop sampling and keep current factor.
    if (done != null) return n * done;
    var abs = Math.abs(n);
    // Insert value (sorted in ascending order).
    outer: do { // Just used for break goto
      for (var i = 0; i < distribution.length; ++i) {
        if (abs <= distribution[i]) {
          distribution.splice(i, 0, abs);
          break outer;
        }
      }
      distribution.push(abs);
    } while (false);
    // Factor is scale divided by 33rd percentile.
    var factor = scale / distribution[Math.floor(distribution.length / 3)];
    if (distribution.length == 500) done = factor;
    return n * factor;
  };
}();

// Usual boilerplate scroll-wheel incompatibility plaster.

var div = document.getElementById("thing");
div.addEventListener("DOMMouseScroll", grabScroll, false);
div.addEventListener("mousewheel", grabScroll, false);

function grabScroll(e) {
  var dx = -(e.wheelDeltaX || 0), dy = -(e.wheelDeltaY || e.wheelDelta || 0);
  if (e.detail != null) {
    if (e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
    else if (e.axis == e.VERTICAL_AXIS) dy = e.detail;
  }
  if (dx) {
    var ndx = Math.round(normalizeWheelDelta(dx));
    if (!ndx) ndx = dx > 0 ? 1 : -1;
    div.scrollLeft += ndx;
  }
  if (dy) {
    var ndy = Math.round(normalizeWheelDelta(dy));
    if (!ndy) ndy = dy > 0 ? 1 : -1;
    div.scrollTop += ndy;
  }
  if (dx || dy) { e.preventDefault(); e.stopPropagation(); }
}

1
这个解决方案在Mac上的Chrome浏览器和触控板完全不起作用。 - pyronaur
@Norris 我相信现在可以了。刚发现这个问题,这里的示例在我的Macbook上使用Chrome可以运行。 - Harry Moreno

4

一个简单且有效的解决方案:

private normalizeDelta(wheelEvent: WheelEvent):number {
    var delta = 0;
    var wheelDelta = wheelEvent.wheelDelta;
    var deltaY = wheelEvent.deltaY;
    // CHROME WIN/MAC | SAFARI 7 MAC | OPERA WIN/MAC | EDGE
    if (wheelDelta) {
        delta = -wheelDelta / 120; 
    }
    // FIREFOX WIN / MAC | IE
    if(deltaY) {
        deltaY > 0 ? delta = 1 : delta = -1;
    }
    return delta;
}

3
这是我今天已经花费几个小时来解决的问题,而且不是第一次了 :(
我一直在尝试在“滑动”上汇总值,并查看不同浏览器如何报告值。它们差异很大,Safari在几乎所有平台上报告数量级更大的数字,Chrome报告的比Firefox多得多(例如三倍),Firefox在长期内保持平衡,但在小运动中各平台之间有很大差异(在Ubuntu gnome上,几乎只有+3或-3,似乎是将较小的事件相加,然后发送一个大的“+3”)。
目前找到的解决方案有三种:
1. 已经提到的“仅使用符号”,这会消除任何加速度。 2. 嗅探浏览器到次要版本和平台,并适当调整。 3. Qooxdoo最近实现了一种自适应算法,基本上尝试根据迄今为止收到的最小值和最大值来缩放增量。
Qooxdoo中的想法很好,有效,并且是我目前发现的唯一跨浏览器完全一致的解决方案。
不幸的是,它也倾向于重新归一化加速度。如果你尝试它(在他们的演示中)并以最大速度向上和向下滚动一段时间,你会注意到极快或极慢的滚动基本上产生几乎相同的移动量。相反,如果您重新加载页面并仅缓慢滑动,则会注意到它会滚动得相当快。
这对于像我这样习惯在触摸板上进行有力滚动滑动并期望到达所滚动的内容的顶部或底部的Mac用户来说是令人沮丧的。
更重要的是,由于它根据获得的最大值缩小鼠标速度,因此您的用户尝试加速它的速度越快,它就会变得越慢,而“慢滚动”用户将体验到相当快的速度。
这使得这个(否则很好的)解决方案成为解决方案1的略微更好的实现。
我将解决方案移植到了jquery mousewheel插件中:http://jsfiddle.net/SimoneGianni/pXzVv/ 如果您玩一会儿,您会看到您将开始获得相当均匀的结果,但您也会注意到它很快会+1/-1值。
我现在正在努力改进它,以更好地检测峰值,使它们不会将所有内容“超出比例”。还可以获得0到1之间的浮点值作为增量值,以便有一个连贯的输出。

3

对于触摸设备的缩放支持,请注册gesturestart、gesturechange和gestureend事件并使用event.scale属性。您可以查看示例代码

对于Firefox 17,计划支持桌面和移动版本的onwheel事件(根据MDN文档关于onwheel)。此外,对于Firefox可能有用的是Gecko特定的MozMousePixelScroll事件(尽管这可能已经过时,因为DOMMouseWheel事件现在在Firefox中已过时)。

对于Windows,驱动程序本身似乎会生成WM_MOUSEWHEEL、WM_MOUSEHWHEEL事件(以及触摸板平移的WM_GESTURE事件?)。这就解释了为什么Windows或浏览器本身似乎不会归一化鼠标滚轮事件的值(这可能意味着您无法编写可靠的代码来归一化这些值)。

对于onwheel事件(不是onmousewheel),在IE9和IE10中支持,您还可以使用W3C标准onwheel事件。然而,一个刻度可能与120不同(例如,在我的鼠标上,单个刻度变为111(而不是-120)使用此测试页面)。我写了另一篇文章,介绍其他可能相关的滚轮事件细节。

基本上在我的滚轮事件测试中(我试图规范滚动的值),我发现在不同的操作系统、浏览器厂商、浏览器版本、事件类型和设备(Microsoft倾斜鼠标、笔记本电脑触摸板手势、带有滚动区域的笔记本电脑触摸板、苹果魔术鼠标、苹果强大的鼠标滚轮、Mac触摸板等等)中,我得到了各种不同的值。

而且我必须忽略来自浏览器配置(例如Firefox的mousewheel.enable_pixel_scrolling,chrome的--scroll-pixels=150)、驱动程序设置(例如Synaptics触摸板)和操作系统配置(Windows鼠标设置、OSX鼠标首选项、X.org按钮设置)的各种副作用。


1

在所有操作系统和浏览器中,肯定没有一种简单的方法可以对所有用户进行标准化。

它比您列出的变体更糟糕 - 在我的WindowsXP + Firefox3.6设置中,我的鼠标滚轮每次滚动一个缺口就会滚动6个 - 可能是因为我在某个地方忘记了我已经加速了鼠标滚轮,无论是在操作系统中还是在about:config中。

然而,我正在解决类似的问题(顺便说一下,是非画布应用程序),我想到只需使用+1 / -1的增量符号并测量时间上次触发的时间,您将获得加速率,即如果有人滚动一次在几秒钟内多次滚动(我敢打赌这就是谷歌地图的做法)。

在我的测试中,这个概念似乎运作良好,只需使任何小于100ms的内容添加到加速度即可。


-2
var onMouseWheel = function(e) {
    e = e.originalEvent;
    var delta = e.wheelDelta>0||e.detail<0?1:-1;
    alert(delta);
}
$("body").bind("mousewheel DOMMouseScroll", onMouseWheel);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接