“线”在滚轮事件中指的是什么高度?(deltaMode = DOM_DELTA_LINE)

45
在Firefox >= 17版本中,wheel事件有一个deltaMode属性。对于我使用的操作系统/鼠标,它被设置为1(或DOM_DELTA_LINE)。这个设置意味着deltaXdeltaY事件值是以行而不是像素来衡量的。果然,如果我假装这些增量是像素,滚动速度比在Firefox中通常要慢得多。

相比之下,Chrome 31使用deltaMode0(或DOM_DELTA_PIXEL),这使我能够模拟正常速度的滚动。
如果我可以将行值转换为像素值,那就一切妥当了。但是我找不到任何关于"行"的文档。我尝试在Firefox中更改font-sizeline-height,但未改变滚动行为。
有知道"行"如何定义的人吗?W3C只说:"这适用于许多表单控件。"
相关链接:W3C deltaModeMDN WheelEventMDN wheel

编辑:这里有一个演示,以说明奇怪的情况。当Firefox处于DOM_DELTA_LINE模式时,像素和行之间没有一致的比例 - 它到处都是。而当我使用触摸板而不是鼠标,导致Firefox切换到DOM_DELTA_PIXEL模式时,也没有一致的比例。另一方面,在Chrome 31中,比例几乎总是非常接近于DOM_DELTA_PIXEL模式下的1:1。

Chromium问题:实现DOM3滚轮事件

Bugzilla错误:实现DOM3滚轮事件

更新:在Firefox中使用单击鼠标滚轮向上或向下滚动,其中deltaModeDOM_DELTA_LINE时,像素的增量取决于CSS font-size,但与line-height无关。请参见此fiddle进行演示。该行为仅在缓慢滚动鼠标滚轮时保持。在速度或动力方面,线条到像素的比例在任何特定实例或聚合中都不可预测。据我所知,没有办法使用DOM_DELTA_LINE模式提供的增量测量来模拟Firefox的滚动行为。

DOM_DELTA_PIXEL模式下,行为几乎完美。也就是说,实际滚动的像素与报告的像素增量值之间的比率几乎完全相等,这在同一个fiddle中得到了证明。

我向 Mozilla 提交了一个错误报告指出在DOM_DELTA_LINE模式下的wheel事件的行为并不有用,因为它是不可预测的(即它是一个方程,其中单位和大小都是变量)。尽管 Firefox 本身并不支持这些增量,但该问题已被标记为无效,因为wheel事件的期望行为是通过操作系统提供的本机增量传递。

我会让这个问题保持开放状态,希望DOM_DELTA_LINE能够在某个规范中得到定义。据我所知,对于font-size(而不是line-height)的依赖性还没有被描述在任何地方。


2
请参考此相关答案,建议使用来自Facebook的一些代码,其中LINE_HEIGHT = 40,PAGE_HEIGHT = 800 https://dev59.com/-W035IYBdhLWcg3wQtog#30134826。此外,还有一个可供使用的npm软件包,可以提取此代码。有关更多详细信息,请参见此处https://www.npmjs.com/package/normalize-wheel。 - Simon Watson
5个回答

26

概述:

虽然由DOM_DELTA_LINE产生的滚动值可能没有被任何规范明确定义,但基于来自Chromium问题跟踪器的以下评论和我的观察,似乎Firefox是目前唯一报告带有除deltaMode以外的其他内容的滚轮事件的浏览器,这些内容为DOM_DELTA_PIXEL0)。

来自Chromium问题实现DOM3滚轮事件评论

We ended up doing what IE does and report the exact number of pixels we would scroll the element.

The following asserts are always true (given that the element can be scrolled).

element.scrollTop = 0;
element.addEventListener('wheel', function(e) {
  assert(e.deltaMode === MouseEvent. DOM_DELTA_PIXEL);
  assert(element.scrollTop === e.deltaY);
});
// scroll

This way you know exactly how much to scroll at all times.

We never set the deltaMode to anything else but DOM_DELTA_PIXEL.

根据该评论,Chromium 与 IE 的像素差异相匹配。虽然没有涵盖到其他浏览器,但这几乎肯定直接适用于现代版的 Opera 和 Safari。在我使用多种输入设备测试 Windows、Mac 和 Linux 平台时,我的观察并未证明这一点有误。
因此,由于 Firefox 是唯一会报告 deltaModeDOM_DELTA_LINE (1) 或 DOM_DELTA_PAGE (2) 的浏览器(目前),我们只需要知道 Firefox 如何计算这些值。尽管这有点棘手,但通过搜索 Firefox 源代码和一些试错,我已经确定它直接对应默认字体和默认字体大小,具体取决于以下首选项的配置,忽略页面上的任何 CSS。

Firefox fonts preferences options

这意味着要正确检测滚动中使用的行高,您必须拥有完全未样式化的内联元素以检测计算渲染高度。由于页面上的CSS几乎肯定会干扰,因此我们必须在iframe中创建一个新的、干净的窗口来进行检测。
检测DOM_DELTA_LINE触发的滚动事件所使用的行高: 为此,我创建了以下小函数,以同步检测纯、未更改的行高。
function getScrollLineHeight() {
    var r;
    var iframe = document.createElement('iframe');
    iframe.src = '#';
    document.body.appendChild(iframe);
    var iwin = iframe.contentWindow;
    var idoc = iwin.document;
    idoc.open();
    idoc.write('<!DOCTYPE html><html><head></head><body><span>a</span></body></html>');
    idoc.close();
    var span = idoc.body.firstElementChild;
    r = span.offsetHeight;
    document.body.removeChild(iframe);
    return r;
}

// Get the native croll line height.
console.log(getScrollLineHeight());

现在我们知道行高差应该转换成多少像素了。然而,在Windows上,还有一件事情需要考虑。在Windows上,默认的滚动速度被覆盖为默认情况下快2倍,但仅适用于根元素。 覆盖系统滚动速度的系统: 我们提供了一个系统滚动速度的覆盖机制,因为Windows的默认系统滚动速度比WebKit的滚动速度慢。这是为了替代加速系统(见下一节)而建议的。
这在Windows的默认设置中启用,mousewheel.system_scroll_override_on_root_content.enabled开关打开时才会生效。
在Windows上,只有当系统滚动速度设置未被用户或鼠标驱动程序自定义时,才会覆盖滚动速度。而在其他情况下,它总是覆盖速度。
可以通过隐藏偏好来指定比例。 mousewheel.system_scroll_override_on_root_content.vertical.factor 用于垂直滚动事件,mousewheel.system_scroll_override_on_root_content.horizontal.factor 用于水平滚动事件。这些值被用作1/100(即默认值200表示2.0)。
当nsEventStateManager执行文档的根可滚动视图的滚动时,它将滚动速度乘以比例。因此,DOMMouseScroll事件的delta值从未被此覆盖。
另请参见bug 513817
这仅适用于文档的根默认滚动元素,例如textarea内部的滚动元素不受影响(除非是iframes?)。如果默认的2倍被重新配置,则也没有办法获取指定的值,因此,如果您认为这个值很重要,您将不得不采取用户代理操作系统嗅探的方法,可能使用技术上非标准但流行的navigator.platform

DOM_DELTA_PAGE呢?

Windows还有一个垂直滚动的备选设置,其中它不是按行数滚动,而是滚动一个几乎完整的页面。澄清一下,在Windows 7中,“每次一个屏幕”设置控制着这个对话框。

Windows 7 wheel settings

当此功能被激活时,单击一次将滚动几乎整个页面,这里指滚动面板的高度减去滚动条。我说几乎,因为Firefox会考虑其他一些因素来减少滚动量。即通过减少一些行、百分比,并以某种方式减去固定位置的页眉和页脚。请参见以下代码块中的一些逻辑。
摘自layout/generic/nsGfxScrollFrame.cpp
nsSize
ScrollFrameHelper::GetPageScrollAmount() const
{
  nsSize lineScrollAmount = GetLineScrollAmount();
  nsSize effectiveScrollPortSize;
  if (mIsRoot) {
    // Reduce effective scrollport height by the height of any fixed-pos
    // headers or footers
    nsIFrame* root = mOuter->PresContext()->PresShell()->GetRootFrame();
    effectiveScrollPortSize =
      GetScrollPortSizeExcludingHeadersAndFooters(root, mScrollPort);
  } else {
    effectiveScrollPortSize = mScrollPort.Size();
  }
  // The page increment is the size of the page, minus the smaller of
  // 10% of the size or 2 lines.
  return nsSize(
    effectiveScrollPortSize.width -
      std::min(effectiveScrollPortSize.width/10, 2*lineScrollAmount.width),
    effectiveScrollPortSize.height -
      std::min(effectiveScrollPortSize.height/10, 2*lineScrollAmount.height));
}
我不完全确定哪些被检测为固定位置的页眉和页脚元素,这应该可以很好地概述DOM_DELTA_PAGE模式的工作方式,除了这个问题所询问的DOM_DELTA_LINE。请注意保留HTML标签。

4
我在Linux上使用Firefox 25测试了您的代码,还对其进行了扩展,只在wheel事件中跟踪scrollTop。在您的代码中,滚动增量是在scroll事件中计算的,这比wheel事件触发的频率要高得多。而且,在软滚动模式下,滚动事件似乎滞后于wheel事件。(即,如果您在软滚动尚未完成时测量,则会获得scrollTop()的“中间”值。)我认为这就是您没有得到滚动增量和wheel事件之间良好相关性的原因。(注意:wheel事件似乎在关联的滚动事件之前处理。)
如果关闭软滚动,则每个滚动事件至少有一个wheel事件。您的示例代码在“wheeling”期间给出了完美的固定值“px/DOM_DELTA_LINE: 18”。如果我添加一个带有两个span的div元素<span id="lineN">N</span>,并在它们之间插入一个<br/>,然后测量($("#line2").position().top - $("#line1").position().top),那么我将得到18。
总结:wheel事件允许计算以像素为单位的滚动距离-至少在我的环境中是如此。希望您的环境也是这样。
以下是我的扩展代码:
var DeltaModes = {
    0: "DOM_DELTA_PIXEL",
    1: "DOM_DELTA_LINE",
    2: "DOM_DELTA_PAGE"
};
var statsPane = $(".stats-pane");
var scrollTop = $(window).scrollTop();
var scrollDelta = 0;
var wheelEvents = 0;
var scrollEvents = 0;
var scrollTopOnWheel = 0;
$(window).scroll(function(){
    scrollEvents++;
    var newScrollTop = $(window).scrollTop();
    scrollDelta = scrollTop - newScrollTop;
    scrollTop = newScrollTop;
});
$(window).on("wheel", function(e){
    wheelEvents++;
    var wheelScrollTop = $(window).scrollTop();
    var wheelScrollDelta = wheelScrollTop - scrollTopOnWheel;
    e = e.originalEvent;
    var pxPerDeltaUnit = Math.abs(scrollDelta) / Math.abs(e.deltaY);
    var deltaMode = DeltaModes[e.deltaMode];
    var stats = [deltaMode + ": " + e.deltaY];
    stats.push("px delta: " + scrollDelta);
    stats.push("px/" + deltaMode + ": " + pxPerDeltaUnit);
    stats.push("wheel scroll top (prev): " + scrollTopOnWheel);
    stats.push("wheel scroll top: " + wheelScrollTop);
    stats.push("wheel scroll delta: " + wheelScrollDelta);
    stats.push("line height: " + ($("#line2").position().top - $("#line1").position().top));
    stats.push("wheel event#: " + wheelEvents);
    stats.push("scroll event#: " + scrollEvents);
    statsPane.html('<div>' + stats.join('</div><div>') + '</div>');
    scrollTopOnWheel = wheelScrollTop;
    //scrollTop = newScrollTop;
});

并且HTML:

<div class="stats-pane"></div>
<div>Hey.</div>
<div>Hi.</div>
<div>Hello.</div>
<div>
    <span id="line1">1</span><br/>
    <span id="line2">2</span><br/>
</div>

有趣的是,我将scrollTop计算移到了wheel事件回调中,并完全删除了scroll监听器。在Mac上,在Firefox 25上,我仍然会得到一个变化很大的px / DOM_DELTA_LINE值。每次滚动鼠标一格,我通常会得到大约10px,20px和30px左右的值。即使禁用平滑滚动,我仍然会得到变化很大的值。http://jsfiddle.net/neonsilk/Zd7yB/3/ - JKS
很遗憾,这里没有Mac可以进行调查。你可能想看一下Gecko:Mouse Wheel Scrolling。它包含有关鼠标滚轮相关功能(例如“加速系统”)和配置设置的信息。 - halfbit

3

现在有一种稍微简单的方法来获取滚动行高(浏览器默认字体大小),而不需要使用 iframe,可以使用 font-size: initial

function getScrollLineHeight() {
  const el = document.createElement('div');
  el.style.fontSize = 'initial';
  el.style.display = 'none';
  document.body.appendChild(el);
  const fontSize = window.getComputedStyle(el).fontSize;
  document.body.removeChild(el);
  return fontSize ? window.parseInt(fontSize) : undefined;
}

// Get the native scroll line height
console.log(getScrollLineHeight());

我在最新版本的Chrome、Firefox、Safari(测试了9和12)、Edge和IE11上进行了测试,这是在覆盖htmlbody元素字体大小的页面上测试的。除了IE11以外,所有其他已测试的浏览器似乎都可以可靠地工作。在IE11上,它将返回从body继承的字体大小,因为IE11不支持CSS关键字initial。所以如果您不再需要支持IE11,那么这应该可以工作。

1
谢谢。在我看来(对于我的用例),这已经完全足够了。由于IE 11无论如何都使用DOM_DELTA_PIXEL,因此当仍需要支持IE 11时,这并不是一个阻碍。 - alex3683

-3

我还没有看过你的代码,但这是我解决问题的方法,也导致我来到了这里。

你可以尝试使用自定义滚动条来覆盖浏览器的滚动条。

window.addEventListener("wheel", function(e){
 if(e.deltaY > 0)
   window.scroll(0,20);
 else
   window.scroll(0,-20)

 e.preventDefault();
});

这样你就可以始终确信滚动的像素数量。


我不明白所有的踩票,这个答案实际上让我找到了正确的方向:在大多数情况下,轮子的 delta 实际上并不重要,除了它的符号。 - Martin Cremer
虽然这对于传统的鼠标滚轮来说已经足够了,但在使用触摸板(或指点杆)滚动、非离散式(高分辨率)鼠标滚轮等设备时,它并不能正常工作。这些设备会产生更多的事件,因此每个事件只应该滚动一点点(这就是delta的作用...)。 - Ghost4Man

-6
我发现使用以下代码片段可以解决问题。它会规范化 deltaX 和 deltaY。试试这个吧。
function onWheel(e) {
    var deltaX = Math.max(-1, Math.min(1, (e.deltaX)));
    var deltaY = Math.max(-1, Math.min(1, (e.deltaY)));
    console.log('deltaX: ' + deltaX + ', deltaY: ' + deltaY);
}

其中eWheelEvent对象。


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