交互式媒体特性及其潜在问题(对于错误的假设)

Avatar of Patrick H. Lauke
Patrick H. Lauke

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

这是 2015 年最初在 dev.opera 上发表的 文章 的更新版本,并进行了大幅扩展。那篇文章引用了 2015 年 3 月 24 日的规范草案,媒体查询级别 4,并且对浏览器在实践中如何评估 any-hover:none 存在相当大的误解。

该规范自那时起已更新(包括我在发布原始文章后提交的澄清和示例),因此此更新版本删除了原始文章中的不正确信息,并将解释与最新的工作草案相一致。它还涵盖了与 JavaScript 触控/输入检测相关的其他方面。

媒体查询级别 4 交互式媒体特性pointerhoverany-pointerany-hover — 用于允许网站根据用户输入设备的特定特性来实现不同的样式和功能(无论是特定于 CSS 的交互性,如 :hover,还是使用 window.matchMedia 查询的 JavaScript 行为)。

尽管规范仍处于工作草案阶段,但交互式媒体特性通常 得到良好支持,尽管到目前为止,各种浏览器实现之间仍然存在一些问题和不一致之处 - 请参阅最近的 pointer/hover/any-pointer/any-hover 测试结果,其中包含对相关浏览器错误的引用。

为交互式媒体特性引用的常见用例通常是“根据用户是否使用触摸屏设备或使用鼠标/触控笔来调整控件的大小”和“只有当用户拥有允许基于悬停的交互的输入时,才使用 CSS 下拉菜单”。

@media (pointer: fine) {
  /* using a mouse or stylus - ok to use small buttons/controls */
}
@media (pointer: coarse) {
  /* using touch - make buttons and other "touch targets" bigger */
}
@media (hover: hover) {
  /* ok to use :hover-based menus */
}
@media (hover: none) {
  /* don't use :hover-based menus */
}

还有一些开发人员使用这些新的交互式媒体特性来实现基于标准的“触控检测”,通常只是在识别设备具有粗略指针时监听触控事件。

if (window.matchMedia && window.matchMedia("(pointer:coarse)").matches) {
  /* if the pointer is coarse, listen to touch events */
  target.addEventListener("touchstart", ...);
  // ...
} else {
  /* otherwise, listen to mouse and keyboard events */
  // ...
}

但是,这些方法略显天真,并且源于对这些交互式媒体查询旨在告诉我们什么的误解。

什么是主要输入?

pointerhover 的一个局限性是,它们的设计只公开浏览器认为是主要指针输入的特性。浏览器认为是什么,以及用户实际用作其主要输入的内容可能不同 - 尤其是在设备之间的界限以及它们支持的输入类型变得越来越模糊的时候。

Microsoft Surface with a keyboard, trackpad, external bluetooth mouse, touchscreen.
哪个是“主要”输入?答案可能取决于活动。

一开始,值得注意的是交互式媒体特性只涵盖指针输入(鼠标、触控笔、触摸屏)。它们不提供任何方法来检测用户的主要输入是否为键盘或类似键盘的界面,例如开关控件。理论上,对于键盘用户来说,浏览器可以报告 pointer: none,表明用户的首要输入根本不是指针。但是,在实践中,没有浏览器提供用户指定他们实际上是键盘用户的方法。因此请记住,无论交互式媒体特性查询可能返回什么,都值得确保您的网站或应用程序也适用于键盘用户。

传统上,我们可以说手机或平板电脑的主要输入是触摸屏。但是,即使在这些设备上,用户也可能拥有额外的输入,例如配对的蓝牙鼠标(一项多年来在 Android 上可用的功能,现在在 iPadOS 中受支持,并且肯定会在 iOS 中推出),他们正在使用它作为他们的主要输入。

一部配对蓝牙键盘和鼠标的 Android 手机,屏幕上显示了 Chrome 中的实际鼠标指针和右键单击上下文菜单
一部配对蓝牙键盘、鼠标和 Apple Pencil 的 iPad,屏幕上显示了 Safari 中的鼠标“点”和右键单击上下文菜单

在这种情况下,虽然该设备名义上具有 pointer: coarsehover: none,但用户实际上可能正在使用能够悬停的精细指针设备。同样,如果用户拥有触控笔(例如 Apple Pencil),他们的主要输入仍然可能被报告为触摸屏,但现在他们拥有能够提供精细指针精度的输入,而不是 pointer: coarse

在这些特定情况下,如果网站只是在调整按钮和控件的大小并避免基于悬停的交互,这对用户来说不会构成重大问题:尽管使用精细且支持悬停的鼠标,或者精细但仍然不支持悬停的触控笔,他们将获得针对粗略、不支持悬停的触摸屏的样式和功能。

如果网站使用 pointer: coarse 中的提示进行更剧烈的更改,例如只监听触控事件,那么这对用户来说将是成问题的 - 请参阅有关可能完全破坏体验的错误假设的部分。

但是,考虑相反的情况:一台“普通”台式机或笔记本电脑,配备触摸屏,例如微软的 Surface。在大多数情况下,主要输入将是触控板/鼠标 - 具有 pointer:finehover:hover - 但是用户很可能会使用触摸屏,它具有粗略的指针精度,并且不支持悬停功能。如果样式和功能随后专门根据触控板/鼠标的特性进行调整,用户可能会发现使用粗略、不支持悬停的触摸屏很麻烦或不可能。

特性触摸屏触摸屏 + 鼠标台式机/笔记本电脑台式机/笔记本电脑 + 触摸屏
pointer:coarsetruetruefalsefalse
pointer:finefalsefalsetruetrue
hover:nonetruetruefalsefalse
hover:hoverfalsefalsetruetrue

有关此问题的类似看法,请参阅 Stu Cox 的 “级别 4 媒体查询的优点和缺点”。虽然它指的是规范的更早版本,该版本只包含 pointerhover 以及这些特性需要报告最不具备的功能而不是主要输入设备的要求。

最初的 pointerhover 本身的问题在于它们没有考虑多输入场景,并且它们依赖于浏览器能够正确选择单个主要输入。这就是 any-pointerany-hover 发挥作用的地方。

测试所有输入的功能

any-pointerany-hover 不仅关注主要指针输入,而是报告所有可用指针输入的组合功能。

为了支持多输入场景,其中不同的(基于指针的)输入可能具有不同的特性,any-pointer(理论上还有 any-hover,但正如我们稍后将看到的,这方面毫无用处)的多个值可以匹配,如果不同的输入设备具有不同的特性(与 pointerhover 相比,它们只引用主要指针输入的功能)。在当前的实现中,这些媒体特性通常按如下方式评估

特性触摸屏触摸屏 + 鼠标台式机/笔记本电脑台式机/笔记本电脑 + 触摸屏
any-pointer:coarsetruetruefalsetrue
any-pointer:finefalsetruetruetrue
any-hover:nonetruefalsefalsefalse
any-hover:hoverfalsetruetruetrue
比较 Firefox 在 Android 上仅使用触摸屏和添加蓝牙鼠标时的媒体查询结果。请注意 pointerhover 保持不变,但 any-pointerany-hover 更改为涵盖新的支持悬停的 fine 输入。

回到交互式媒体特性的原始用例,我们可以根据任何可用指针输入的特性,而不是仅根据主要指针输入的特性来决定是否提供更大或更小的输入,或者是否仅启用基于悬停的功能。简而言之,与其说“如果主要输入具有 pointer: coarse,则调整所有控件的大小”或“只有当主要输入具有 hover: hover 时,才提供 CSS 菜单”,我们可以构建等同于“如果任何指针输入是 coarse,则调整控件的大小”和“只有当用户可用的至少一个指针输入支持悬停时,才提供基于悬停的菜单”的媒体查询。

@media (any-pointer: coarse) {
  /* at least one of the pointer inputs
    is coarse, best to make buttons and 
    other "touch targets" bigger (using 
    the query "defensively" to target 
    the least capable input) */
}
@media (any-hover: hover) {
  /* at least one of the inputs is 
     hover-capable, so it's at least 
     possible for users to trigger
     hover-based menus */
}

由于 any-pointerany-hover 的当前定义方式(“用户可用的所有指向设备的功能的并集”),any-pointer: none 只有在没有指针输入可用时才会评估为 true,更重要的是,any-hover: none 只有在没有一个可用的指针输入支持悬停时才会为 true。特别是对于后者,因此不可能使用 any-hover: none 查询来确定当前是否存在一个或多个不支持悬停的指针输入 - 我们只能使用此媒体特性查询来确定所有输入是否都不支持悬停,这可以通过检查 any-hover: hover 是否评估为 false 来实现。这使得 any-hover: none 查询基本上是多余的。

我们可以通过推断如果 any-pointer: coarse 为真,那么很可能是一个触摸屏,并且通常这些输入不支持悬停功能来解决这个问题,但从概念上讲,我们在这里做出了假设,一旦出现一个粗略且支持悬停的指针,这种逻辑就会失效。(对于那些怀疑我们是否会看到支持悬停的触摸屏的人来说,请记住,某些设备,例如三星 Galaxy Note 和微软的 Surface,拥有支持悬停的触控笔,即使它没有接触数字化仪/屏幕也能被检测到,因此某种形式的“悬停触控”检测在未来并非不可能。)

组合查询以获得更明智的猜测

any-pointerany-hover提供的信息当然可以与pointerhover结合使用,以及浏览器对什么主要输入功能的判断,以进行一些稍微细致的评估。

@media (pointer: coarse) and (any-pointer: fine) {
  /* the primary input is a touchscreen, but
     there is also a fine input (a mouse or 
     perhaps stylus) present. Make the design
     touch-first, mouse/stylus users can
     still use this just fine (though it may 
     feel a big clunky for them?) */
}
@media (pointer: fine) and (any-pointer: coarse) {
  /* the primary input is a mouse/stylus,
     but there is also a touchscreen 
     present. May be safest to make 
     controls big, just in case users do 
     actually use the touchscreen? */
}
@media (hover: none) and (any-hover: hover) {
  /* the primary input can't hover, but
     the user has at least one other
     input available that would let them
     hover. Do you trust that the primary
     input is in fact what the user is 
     more likely to use, and omit hover-
     based interactions? Or treat hover 
     as something optional — can be 
     used (e.g. to provide shortcuts) to 
     users that do use the mouse, but 
     don't rely on it? */
}

动态变化

根据规范,浏览器应该响应用户环境的变化重新评估媒体查询。这意味着pointerhoverany-pointerany-hover交互媒体特性可以在任何时候动态变化。例如,在移动/平板设备上添加/删除蓝牙鼠标将触发any-pointer/any-hover的变化。更极端的例子是Surface平板电脑,在添加/删除设备的“键盘盖”(包括键盘和触控板)时,将导致主要输入本身的变化(从键盘盖存在时为pointer: fine/hover: hover,到Surface处于“平板模式”时为pointer: coarse/hover: none)。

Surface平板电脑上Firefox的屏幕截图。 在 连接了 键盘盖的情况下, pointer:finehover:hoverany-pointer:coarseany-pointer:fine,以及 any-hover:hover 为真;一旦键盘盖被移除 (并且 Windows询问用户是否要切换到 “平板 模式”),触摸将成为主要输入, pointer:coarse 和 hover:none,只有 any-pointer:coarse 和 any-hover:none 为真。

如果您正在根据这些媒体特性修改网站的布局/功能,请注意,只要输入发生变化,网站可能会突然在“用户脚下”发生变化——不仅仅是在页面/网站首次加载时。

媒体查询可能不够——继续使用脚本

交互媒体特性的根本缺陷在于,它们不会一定告诉我们现在正在使用的输入设备的任何信息。为此,我们可能需要深入研究一些解决方案,比如What Input?,它会跟踪触发的特定JavaScript事件。但当然,这些解决方案只能在用户开始与网站交互之后才能为我们提供关于用户输入的信息——到那时,可能已经来不及对您的布局或功能进行重大更改了。

请记住,即使这些基于JavaScript的方法也很容易导致错误的结果。这在移动/平板平台上尤其如此,或者在辅助技术参与的情况下,通常会看到生成“伪造”的事件。例如,如果我们查看使用键盘和屏幕阅读器在桌面激活控件时触发的事件序列,我们可以看到触发了假的鼠标事件。辅助技术这样做是因为,历史上,很多网页内容都是针对鼠标用户编写的,但并不一定针对键盘用户,因此对于某些功能,需要模拟这些交互。

同样,在iOS的设置→辅助功能→键盘中启用“完全键盘支持”时,用户可以使用外部蓝牙键盘导航网页内容,就像他们在桌面一样。但是,如果我们查看移动/平板设备和配对的键盘/鼠标的事件序列,这种情况会产生指针事件触摸事件回退鼠标事件——与触摸屏交互得到的相同序列。

Showing iOS settings with Full Keyboard Access enabled on the left and an iPhone browser window open to the right with the What Input tool.
启用后,iOS的 “完全键盘访问”设置会导致指针、触摸和鼠标事件。What Input? 将其识别为触摸输入

在所有这些情况下,像What Input? 这样的脚本会——可以理解,并非其自身的原因——错误地识别当前的输入类型。

可能导致体验完全破坏的错误假设

在概述了多输入设备的复杂性之后,现在应该清楚,那些只监听特定类型的事件的方法,比如我们在常用中看到的“触摸检测”的形式,很快就会失效。

if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) {
  /* if the pointer is coarse, listen to touch events */
  target.addEventListener("touchstart", ...);
  // ...
} else {
  /* otherwise, listen to mouse and keyboard events */
  target.addEventListener("click", ...);
  // ...
}

对于具有附加输入的“触摸”设备——例如,具有外部鼠标的移动设备或平板电脑——这段代码基本上会阻止用户使用除触摸屏之外的任何东西。而在主要以鼠标驱动的设备上,但具有辅助触摸屏界面——比如微软Surface——用户将无法使用触摸屏。

不要将此视为“触摸鼠标/键盘”,而是将其视为“触摸鼠标/键盘”。如果我们只希望在存在实际触摸屏设备时注册触摸事件,以提高性能,我们可以尝试检测any-pointer: coarse。但我们也应该为鼠标和键盘保留其他常规事件监听器。

/* always, as a matter of course, listen to mouse and keyboard events */
target.addEventListener("click", ...);
 // ...

if (window.matchMedia && window.matchMedia("(any-pointer: coarse)").matches) {
  /* if there's a coarse pointer, *also* listen to touch events */
  target.addEventListener("touchstart", ...);
  // ...
}

或者,我们可以通过使用指针事件来避免关于不同类型事件的整个难题,指针事件在一个统一的事件模型中涵盖了所有类型的指针输入,并且得到了相当广泛的支持

让用户做出明确的选择

巧妙地规避我们无法对用户正在使用的输入类型做出绝对判断的潜在解决方案可能是,使用媒体查询和像What Input? 这样的工具提供的信息,不要立即在不同的布局/功能之间切换——或者更糟糕的是,只监听特定类型的事件,并可能阻止任何其他输入类型——而是仅将它们用作何时为用户提供明确切换模式方式的信号。

例如,请参见Microsoft Office 如何让您在“触摸”和“鼠标”模式之间切换。在触摸设备上,此选项默认显示在应用程序的工具栏中,而在非触摸设备上,它最初是隐藏的(尽管可以启用它,无论是否存在触摸屏)。

Screenshot of Microsoft Office's 'Touch/Mouse mode' dropdown, and a comparison of (part of) the toolbar as it's presented in each mode

网站或 Web 应用程序可以采用相同的方法,甚至可以根据主要输入设置默认值——但仍允许用户明确地更改模式。此外,使用类似于 What Input? 的方法,网站可以检测到触摸输入的首次出现,并在用户想要切换到触摸友好模式时提醒/提示用户。

可能出现错误的假设——谨慎地使用查询

使用媒体查询级别 4 交互媒体特性并根据可用主要或附加指针输入的特性调整我们的网站是一个好主意——但要小心对这些媒体特性实际上说明了什么做出错误的假设。与类似的特性检测方法一样,开发人员需要了解他们到底在尝试检测什么、特定检测的局限性,最重要的是,考虑他们为什么要这样做——类似于我在文章中提到的关于检测触摸的问题

pointerhover告诉我们浏览器确定为主要设备输入的设备的功能。any-pointerany-hover告诉您所有连接的输入的功能,并且与关于主要指针输入的信息相结合,它们允许我们对用户的特定设备/场景做出明智的猜测。我们可以使用这些特性来告知我们的布局,或者我们想提供的交互/功能类型;但不要忽视这些假设可能不正确的可能性。媒体查询本身不一定有缺陷(尽管大多数浏览器似乎仍然存在一些怪癖和错误,增加了潜在的问题)。这取决于它们的具体用法。

有了这些,我想最后提出一些建议,以“保护”您免受输入检测陷阱的危害。

不要

假设单一的输入类型。现在不是“触摸鼠标/键盘”,而是“触摸鼠标/键盘”——而且可用的输入类型可能在任何时候都会发生变化,即使是在初始页面加载之后。

仅仅根据pointerhover“主要”指针输入不一定就是您的用户正在使用的输入。

一般来说,请依靠hover 不管hoverany-hover 表明了什么,您的用户可能正在使用一个不支持悬停的指针输入,而您目前无法检测到这一点,除非它是主要的输入(因为hover: none 如果特定输入缺乏悬停功能,则为 true,但any-hover: none 只有在所有输入都不支持悬停功能时才为 true)。请记住,基于悬停的界面通常不适合键盘用户。

使您的界面“触摸友好”。 如果您检测到存在any-pointer:coarse 输入(最有可能的是触摸屏),请考虑提供较大的触摸目标,并在目标之间留出足够的间距。即使用户此时使用的是其他输入,比如鼠标,也没关系。

给用户一个选择。 如果其他方法都失败,请考虑给用户一个选项/切换,让他们在触摸布局和鼠标布局之间切换。您可以随意使用从媒体查询中获取的信息(例如any-pointer: coarse 为 true),以便对切换的初始设置进行有根据的猜测。

记住键盘用户。 无论用户是否使用任何指针输入,都不要忘记键盘可访问性——无法最终检测到,因此,请确保您的内容默认情况下适合键盘用户。