我正在开发一个 web 应用程序(不是一个包含有趣文本页面的网站),它具有非常不同的触摸界面(单击时手指隐藏屏幕)和鼠标(严重依赖悬停预览)。我怎样才能检测到我的用户没有鼠标来向他展示正确的界面?我打算为同时使用鼠标和触摸的人(如某些笔记本)留下一个开关。
浏览器中的触摸事件功能实际上并不意味着用户正在使用触摸设备(例如,Modernizr 没有削减它)。如果设备有鼠标,则正确回答问题的代码应返回 false,否则返回 true。对于具有鼠标和触摸功能的设备,它应该返回 false(不是仅触摸)
附带说明一下,我的触摸界面也可能适用于纯键盘设备,因此我希望检测到的更多的是缺少鼠标。
为了使需求更清楚,这是我希望实现的 API:
// Level 1
// The current answers provide a way to do that.
hasTouch();
// Returns true if a mouse is expected.
// Note: as explained by the OP, this is not !hasTouch()
// I don't think we have this in the answers already, that why I offer a bounty
hasMouse();
// Level 2 (I don't think it's possible, but maybe I'm wrong, so why not asking)
// callback is called when the result of "hasTouch()" changes.
listenHasTouchChanges(callback);
// callback is called when the result of "hasMouse()" changes.
listenHasMouseChanges(callback);
截至 2018 年,有一种很好且可靠的方法来检测浏览器是否有鼠标(或类似的输入设备):现在几乎所有现代浏览器(IE 11 和特殊的移动浏览器除外)都支持的 CSS4 媒体交互功能。
W3C:
指针媒体功能用于查询指针设备(例如鼠标)的存在和准确性。
请参阅以下选项:
/* The primary input mechanism of the device includes a
pointing device of limited accuracy. */
@media (pointer: coarse) { ... }
/* The primary input mechanism of the device
includes an accurate pointing device. */
@media (pointer: fine) { ... }
/* The primary input mechanism of the
device does not include a pointing device. */
@media (pointer: none) { ... }
/* Primary input mechanism system can
hover over elements with ease */
@media (hover: hover) { ... }
/* Primary input mechanism cannot hover
at all or cannot conveniently hover
(e.g., many mobile devices emulate hovering
when the user performs an inconvenient long tap),
or there is no primary pointing input mechanism */
@media (hover: none) { ... }
/* One or more available input mechanism(s)
can hover over elements with ease */
@media (any-hover: hover) { ... }
/* One or more available input mechanism(s) cannot
hover (or there are no pointing input mechanisms) */
@media (any-hover: none) { ... }
媒体查询也可以在 JS 中使用:
if(window.matchMedia("(any-hover: none)").matches) {
// do something
}
有关的:
W3 文档:https://www.w3.org/TR/mediaqueries-4/#mf-interaction
浏览器支持:https://caniuse.com/#search=media%20features
类似问题:Detect if a client device supports :hover and :focus states
主要问题是您有以下不同类别的设备/用例:
鼠标和键盘(台式机) 仅限触控(手机/平板电脑) 鼠标、键盘和触控(触控笔记本电脑) 触控和键盘(平板电脑上的蓝牙键盘) 仅限鼠标(禁用用户/浏览首选项) 仅键盘(禁用用户/浏览首选项)触摸和鼠标(即来自 Galaxy Note 2 笔的悬停事件)
更糟糕的是,可以从其中一些类转换到其他类(插入鼠标,连接到键盘),或者用户可能会出现在普通笔记本电脑上,直到他们伸出手触摸屏幕。
您假设浏览器中存在事件构造函数不是前进的好方法是正确的(并且它有些不一致)。此外,除非您正在跟踪一个非常具体的事件或只是试图排除上述几个类别,否则使用事件本身并不是完全的证明。
例如,假设您发现用户发出了真正的 mousemove(不是来自触摸事件的错误,请参阅 http://www.html5rocks.com/en/mobile/touchandmouse/)。
然后呢?
您启用悬停样式?您添加更多按钮?
无论哪种方式,您都在增加玻璃的时间,因为您必须等待事件触发。
但是,当您的高贵用户决定拔下鼠标并完全触摸时会发生什么......您是否等待他触摸您现在拥挤的界面,然后在他努力确定您现在拥挤的 UI 之后立即更改它?
以项目符号的形式,在 https://github.com/Modernizr/Modernizr/issues/869#issuecomment-15264101 引用 stucox
我们想检测鼠标的存在我们可能无法在事件触发之前检测到它因此,我们正在检测的是是否在此会话中使用了鼠标——它不会立即从页面加载我们可能也无法检测到没有鼠标——它在为真之前是未定义的(我认为这比在被证明之前将其设置为假更有意义)而且我们可能无法检测到鼠标是否在会话中断开连接——这与用户只是放弃鼠标没有什么区别
顺便说一句:浏览器确实知道用户何时插入鼠标/连接到键盘,但不会将其暴露给 JavaScript .. dang!
这应该会引导您完成以下操作:
跟踪给定用户的当前能力是复杂、不可靠且值得怀疑的
不过,渐进增强的想法在这里非常适用。无论用户所处的环境如何,都可以打造顺畅运行的体验。然后根据浏览器功能/媒体查询做出假设,以添加在假设上下文中相关的功能。鼠标的存在只是不同设备上的不同用户体验您的网站的众多方式之一。在其内核中创建具有优点的东西,不要太担心人们如何点击按钮。
:hover
元素等)是有意义的。用户当前似乎不太可能同时使用鼠标+触摸(我的意思是comoonn就像将2个鼠标连接到同一台计算机哈哈哈)
如何监听文档上的 mousemove 事件。然后,直到您听到该事件,您假设设备仅是触摸或键盘。
var mouseDetected = false;
function onMouseMove(e) {
unlisten('mousemove', onMouseMove, false);
mouseDetected = true;
// initializeMouseBehavior();
}
listen('mousemove', onMouseMove, false);
(其中 listen
和 unlisten
酌情委托给 addEventListener
或 attachEvent
。)
希望这不会导致太多的视觉卡顿,如果您需要基于模式的大量重新布局,那会很糟糕......
@Wyatt 的回答很棒,给了我们很多思考。
就我而言,我选择倾听第一次交互,然后才设置行为。因此,即使用户有鼠标,如果第一次交互是触摸,我也会将其视为触摸设备。
考虑 given order in which events are processed:
touchstart touchmove touchend mouseover mousemove mousedown mouseup click
我们可以假设如果鼠标事件在触摸之前被触发,它是一个真实的鼠标事件,而不是模拟的。示例(使用 jQuery):
$(document).ready(function() {
var $body = $('body');
var detectMouse = function(e){
if (e.type === 'mousedown') {
alert('Mouse interaction!');
}
else if (e.type === 'touchstart') {
alert('Touch interaction!');
}
// remove event bindings, so it only runs once
$body.off('mousedown touchstart', detectMouse);
}
// attach both events to body
$body.on('mousedown touchstart', detectMouse);
});
这对我有用
只能检测浏览器是否支持触摸。没有办法知道它是否真的连接了触摸屏或鼠标。
如果检测到触摸功能,则可以通过侦听触摸事件而不是鼠标事件来确定使用的优先级。
跨浏览器检测触摸功能:
function hasTouch() {
return (('ontouchstart' in window) || // html5 browsers
(navigator.maxTouchPoints > 0) || // future IE
(navigator.msMaxTouchPoints > 0)); // current IE10
}
然后可以使用它来检查:
if (!hasTouch()) alert('Sorry, need touch!);
或选择要收听的事件,或者:
var eventName = hasTouch() ? 'touchend' : 'click';
someElement.addEventListener(eventName , handlerFunction, false);
或对触摸与非触摸使用单独的方法:
if (hasTouch() === true) {
someElement.addEventListener('touchend' , touchHandler, false);
} else {
someElement.addEventListener('click' , mouseHandler, false);
}
function touchHandler(e) {
/// stop event somehow
e.stopPropagation();
e.preventDefault();
window.event.cancelBubble = true;
// ...
return false; // :-)
}
function mouseHandler(e) {
// sorry, touch only - or - do something useful and non-restrictive for user
}
对于鼠标,只能检测是否正在使用鼠标,而不能检测它是否存在。可以设置一个全局标志来指示鼠标被使用检测到(类似于现有答案,但简化了一点):
var hasMouse = false;
window.onmousemove = function() {
hasMouse = true;
}
(不能包含 mouseup
或 mousedown
,因为这些事件也可以通过触摸触发)
浏览器限制了对低级系统 API 的访问,这些 API 是检测正在使用的系统的硬件功能等特性所必需的。
有可能编写一个插件/扩展来访问这些,但通过 JavaScript 和 DOM,这种检测仅限于此目的,并且必须编写一个特定于各种操作系统平台的插件。
所以总而言之:这种检测只能通过“好的猜测”来估计。
当 Media Queries Level 4 在浏览器中可用时,我们将能够使用“指针”和“悬停”查询来检测带有鼠标的设备。
如果我们真的想将该信息传达给 Javascript,我们可以使用 CSS 查询根据设备类型设置特定样式,然后使用 Javascript 中的 getComputedStyle
读取该样式,并从中派生出原始设备类型。
但是鼠标可以随时连接或拔出,用户可能想要在触摸和鼠标之间切换。所以我们可能需要检测这种变化,并提供改变界面或自动这样做。
any-pointer
and any-hover
将让您调查所有适用的设备功能。很高兴看到我们将来如何解决这个问题! :)
既然您打算提供一种在界面之间切换的方法,那么简单地要求用户单击链接或按钮以“输入”应用程序的正确版本是否可行?然后你可以记住他们对未来访问的偏好。它不是高科技,但它是 100% 可靠的 :-)
@SamuelRossille 不幸的是,我知道没有浏览器会暴露鼠标的存在(或缺少)。
因此,话虽如此,我们只需要尝试并使用我们现有的选项尽力而为......事件。我知道这不是你要找的……同意它目前远非理想。
我们可以尽最大努力确定用户是否在任何给定时刻使用鼠标或触摸。这是一个使用 jQuery & Knockout 的快速而肮脏的例子:
//namespace
window.ns = {};
// for starters, we'll briefly assume if touch exists, they are using it - default behavior
ns.usingTouch = ko.observable(Modernizr.touch); //using Modernizr here for brevity. Substitute any touch detection method you desire
// now, let's sort out the mouse
ns.usingMouse = ko.computed(function () {
//touch
if (ns.usingTouch()) {
//first, kill the base mousemove event
//I really wish browsers would stop trying to handle this within touch events in the first place
window.document.body.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
}, true);
//remove mouse class from body
$('body').removeClass("mouse");
//switch off touch listener
$(document).off(".ns-touch");
// switch on a mouse listener
$(document).on('mousemove.ns-mouse', function (e) {
if (Math.abs(window.lastX - e.clientX) > 0 || window.lastY !== e.clientY) {
ns.usingTouch(false); //this will trigger re-evaluation of ns.usingMouse() and result in ns.usingMouse() === true
}
});
return false;
}
//mouse
else {
//add mouse class to body for styling
$('body').addClass("mouse");
//switch off mouse listener
$(document).off(".ns-mouse");
//switch on a touch listener
$(document).on('touchstart.ns-touch', function () { ns.usingTouch(true) });
return true;
}
});
//tests:
//ns.usingMouse()
//$('body').hasClass('mouse');
您现在可以绑定/订阅 usingMouse() 和 usingTouch() 和/或使用 body.mouse 类设置界面样式。一旦检测到鼠标光标并在 touchstart 上,界面就会来回切换。
希望我们很快就会从浏览器供应商那里得到一些更好的选择。
在类似的情况下,这对我有用。基本上,假设用户没有鼠标,直到您看到一系列连续的鼠标移动,而没有干预 mousedowns 或 mouseups。不是很优雅,但它有效。
var mousedown = false;
var mousemovecount = 0;
function onMouseDown(e){
mousemovecount = 0;
mousedown = true;
}
function onMouseUp(e){
mousedown = false;
mousemovecount = 0;
}
function onMouseMove(e) {
if(!mousedown) {
mousemovecount++;
if(mousemovecount > 5){
window.removeEventListener('mousemove', onMouseMove, false);
console.log("mouse moved");
$('body').addClass('has-mouse');
}
} else {
mousemovecount = 0;
}
}
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);
为什么不检测它是否具有感知触摸和/或对鼠标移动做出反应的能力?
// This will also return false on
// touch-enabled browsers like Chrome
function has_touch() {
return !!('ontouchstart' in window);
}
function has_mouse() {
return !!('onmousemove' in window);
}
Tera-WURFL 可以通过将浏览器签名与其数据库进行比较,告诉您访问您网站的设备的功能。给它看看,它是免费的!
我遇到了同样的问题,一次触摸也被注册为点击。在阅读了投票最多的答案的评论后,我想出了自己的解决方案:
var body = document.getElementsByTagName('body')[0];变量鼠标计数 = 0; // 以未定义状态开始 // (一旦我们决定使用什么输入,我就使用它来混合元素) var interactionMode = 'undefined'; var registerMouse = function() { // 每次向上计数 mouseCount,触发 mousemove 事件 mouseCount++; // 但不要立即设置它。 // 而是等待 20 毫秒(对于多个移动操作来说似乎是一个不错的值),// 如果另一个 mousemove 事件作为交互切换到鼠标 setTimeout(function() { // 一个触摸事件也恰好触发 1 个鼠标移动事件。 // 所以只有当 mouseCount 大于 1 时我们才真正通过鼠标移动光标 if (mouseCount > 1) { body.removeEventListener('mousemove', registerMouse); body.removeEventListener('touchend', registerTouch); interactionMode = 'mouse'; console.log('now mousing'); listenTouch(); } // 再次将计数器设置为零 mouseCount = 0; }, 20); }; var registerTouch = function() { body.removeEventListener('mousemove', registerMouse); body.removeEventListener('touchend', registerTouch);交互模式 = '触摸'; console.log('现在触摸');鼠标计数 = 0;听鼠(); }; var listenMouse = function() { body.addEventListener("mousemove", registerMouse); }; var listenTouch = function() { body.addEventListener("touchend", registerTouch); };听鼠();听触(); // 在没有输入的一秒后,假设我们正在触摸 // 可以调整到几秒或删除 // 如果没有这个,interactionMode 将保持“未定义”,直到第一次鼠标或触摸事件 setTimeout(function() { if ( !body.classList.contains('mousing') || body.classList.contains('touching')) { registerTouch(); } }, 1000); /* 修复,以便可以滚动 */ html, body { height: 110%; } 鼠标或触摸我
我发现的唯一问题是,您必须能够滚动才能正确检测到触摸事件。单个选项卡(触摸)可能会产生问题。
正如其他人指出的那样,明确检测他们是否有老鼠是不可靠的。这很容易改变,具体取决于设备。至少在文档规模上,这绝对是您无法使用布尔值 true 或 false 可靠地完成的事情。
触摸事件和鼠标事件是互斥的。所以这可以在一定程度上帮助采取不同的行动。问题是触摸事件更接近鼠标上/下/移动事件,并且还会触发单击事件。
从您的问题中,您说您希望悬停进行预览。除此之外,我不知道有关您的界面的任何其他细节。我假设在没有鼠标的情况下,您希望点击预览,而由于悬停预览,单击会执行不同的操作。
如果是这种情况,您可以采取一些懒惰的方法进行检测:
onclick 事件之前总是会出现一个带有鼠标的 onmouseover 事件。因此请注意,鼠标位于已单击的元素之上。
您可以使用文档范围的 onmousemove 事件来做到这一点。您可以使用 event.target
记录鼠标所在的元素。然后在您的 onclick 事件中,您可以检查鼠标是否实际上位于被单击的元素(或元素的子元素)上。
从那里您可以选择依赖两者的点击事件,并根据结果执行 A 或 B 操作。如果某些触摸设备不发出单击事件(相反,您将不得不依赖 ontouch* 事件),则 B 操作可能什么都不是。
我认为不可能识别仅触摸设备(当然据我所知)。主要问题是所有鼠标和键盘事件也是由触摸设备触发的。请参阅以下示例,对于触摸设备,两个警报都返回 true。
function is_touch_present() {
return ('ontouchstart' in window) || ('onmsgesturechange' in window);
}
function is_mouse_present() {
return (('onmousedown' in window) && ('onmouseup' in window) && ('onmousemove' in window) && ('onclick' in window) && ('ondblclick' in window) && ('onmousemove' in window) && ('onmouseover' in window) && ('onmouseout' in window) && ('oncontextmenu' in window));
}
alert("Touch Present: " + is_touch_present());
alert("Mouse Present: " + is_mouse_present());
'onmousedown' in window
返回 true
我认为最好的主意是 mousemove
侦听器(目前是最佳答案)。我相信这种方法需要稍微调整一下。确实,基于触摸的浏览器甚至会模拟 mousemove 事件,正如您在 this iOS discussion 中看到的那样,所以我们应该小心一点。
有意义的是,基于触摸的浏览器只会在用户点击屏幕(用户的手指向下)时模拟此事件。这意味着我们应该在 mousemove 处理程序期间添加一个测试,以查看在事件期间哪个鼠标按钮按下(如果有)。如果没有按下鼠标按钮,我们可以安全地假设存在真正的鼠标。如果按下鼠标按钮,则测试仍然没有结果。
那么这将如何实施呢?此 question 表明,在 mousemove 期间检查哪个鼠标按钮按下的最可靠方法是实际侦听文档级别的 3 个事件:mousemove、mousedown 和 mouseup。 up 和 down 只会设置一个全局布尔标志。移动将执行测试。如果你有一个动作并且布尔值是假的,我们可以假设鼠标存在。有关确切的代码示例,请参阅问题。
最后一条评论.. 这个测试并不理想,因为它不能在加载时执行。因此,我将使用之前建议的渐进增强方法。默认显示不支持鼠标特定悬停界面的版本。如果发现鼠标,请在运行时使用 JS 启用此模式。这对用户来说应该尽可能无缝。
为了支持更改用户的配置(即鼠标已断开连接),可以定期重新测试。尽管我相信在这种情况下最好简单地通知用户这两种模式并让用户在它们之间手动切换(就像移动/桌面选择总是可以颠倒的)。
在各种 PC、Linux、iPhone、Android 手机和标签上进行了一些测试。奇怪的是没有简单的防弹解决方案。当一些有触摸但没有鼠标的应用程序仍然存在触摸和鼠标事件时,就会出现问题。由于确实希望支持仅鼠标和仅触摸实例,因此希望同时处理两者,但这会导致用户交互的重复出现。如果可以知道设备上不存在鼠标,则可以知道忽略假/插入的鼠标事件。如果遇到 MouseMove,则尝试设置标志,但某些浏览器会抛出假的 MouseMove 以及 MouseUp 和 MouseDown。尝试检查时间戳,但认为这太冒险了。底线:我发现创建假鼠标事件的浏览器总是在插入 MouseDown 之前插入一个 MouseMove。在我 99.99% 的情况下,当在具有真实鼠标的系统上运行时,会有多个连续的 MouseMove 事件——至少两个。因此,跟踪系统是否遇到两个连续的 MouseMove 事件,如果从未满足此条件,则声明不存在鼠标。这可能太简单了,但它适用于我所有的测试设置。我想我会坚持下去,直到找到更好的解决方案。 - 吉姆 W
jQuery 中用于检测鼠标使用的简单解决方案,它解决了移动设备也触发“mousemove”事件的问题。只需添加一个 touchstart 侦听器即可删除 mousemove 侦听器,这样它就不会在触摸时触发。
$('body').one('touchstart.test', function(e) {
// Remove the mousemove listener for touch devices
$('body').off('mousemove.test');
}).one('mousemove.test', function(e) {
// MOUSE!
});
当然,设备仍然可以是触摸和鼠标,但以上将保证使用的是真正的鼠标。
刚刚找到了一个我认为非常优雅的解决方案。
// flag as mouse interaction by default
var isMouse = true;
// detect a touch event start and flag it
$(window).on('touchstart', function () {
// if triggers touchstart, toggle flag
// since touch events come before mouse event / click
// callback of mouse event will get in callback
// `isMouse === false`
isMouse = false;
});
// now the code that you want to write
// should also work with `mouse*` events
$('a.someClass').on('click', function () {
if (isMouse) {
// interaction with mouse event
// ...
} else {
// reset for the next event detection
// this is crucial for devices that have both
// touch screen and mouse
isMouse = true;
// interaction with touch event
// ...
}
});
我花了几个小时为我的 Phonegap 应用程序解决这个问题,然后我想出了这个 hack。如果触发的事件是“被动”事件,它会生成控制台警告,这意味着它不会触发任何更改,但它可以工作!我会对任何改进的想法或更好的方法感兴趣。但这有效地允许我普遍使用 $.touch() 。
$(document).ready(function(){ $("#aButton").touch(function(origElement, origEvent){ console.log('原始目标ID:' + $(origEvent.target).attr('id ')); }); }); $.fn.touch = 函数(回调){ var touch = false; $(this).on("click", function(e){ if (!touch) { console.log("I click!"); let callbackReal = callback.bind(this); callbackReal(this, e); }else{ touch = true; } touch = false; }); $(this).on("touchstart", function(e){ if (typeof e.touches != typeof undefined) { e.preventDefault(); touch = true; console.log("I touch!"); 让callbackReal = callback.bind(this); callbackReal(this, e); } }); }
我在这里看到的主要问题是大多数触摸设备都会触发鼠标事件以及相应的触摸事件(touchstart -> mousedown、touchmove -> mousemove 等)。对于只有键盘的,最后对于现代的,它们具有通用浏览器,因此您甚至无法检测到 MouseEvent 类的存在。
在我看来,这里不那么痛苦的解决方案是在启动时显示一个菜单(对仅键盘用户使用“alt”管理),并可能使用 localStorage/cookies/serverside 存储选择,或者在下一次保持相同的选择访客来的时间。
运行代码片段(如下)来试试这个:
var msg = (window.matchMedia("(any-pointer: coarse)").matches ? "Touchscreen" : "Mouse");
var msg = (window.matchMedia("(any-pointer: rough)").matches ? "Touchscreen" : "Mouse"); document.getElementById('info').innerHTML = msg;身体{背景:黑色;颜色:矢车菊蓝; } #info { 位置:绝对;左:50%;最高:50%;变换:翻译(-50%,-50%);字体大小:30vmin;字体系列:verdana、arial、helvetica; }
(这里有关于 window.matchMedia
的更多信息。)
window.matchMedia("(any-pointer: coarse)").matches
在移动设备上返回 true,在桌面设备上返回 false,但它不考虑连接的鼠标或 s-pen。
我强烈建议不要使用这种方法。考虑一下触摸屏、桌面大小的设备,您需要解决一系列不同的问题。
请让您的应用在没有鼠标的情况下可用(即没有悬停预览)。
通常最好检测是否支持鼠标悬停功能,而不是检测操作系统/浏览器类型。您可以通过以下方式简单地做到这一点:
if (element.mouseover) {
//Supports the mouseover event
}
确保您不执行以下操作:
if (element.mouseover()) {
// Supports the mouseover event
}
后者实际上会调用该方法,而不是检查它的存在。
您可以在此处阅读更多内容:http://www.quirksmode.org/js/support.html
window.matchMedia("(any-pointer: fine)").matches
在我的所有移动浏览器和桌面上都返回 true。window.matchMedia("(any-hover: hover)").matches
也总是返回 true,即使在没有鼠标的移动设备上也是如此。只有window.matchMedia("(any-pointer: coarse)").matches
在移动设备上返回 true,在桌面设备上返回 false,但它不考虑连接的鼠标或 s-pen。