在触摸屏上处理鼠标和触控事件

18

我正在编写一个 Web 应用程序,应该支持鼠标和触摸交互。为了测试,我使用带有 Windows 7 的触摸屏设备。我尝试在最新版本的 Firefox 和 Chrome canary 中嗅探触摸事件,并得到以下结果:

在 Firefox 上,触摸和相应的鼠标事件都会触发。 Chrome 会触发 touchstart/mousedowntouchend/mouseup 成对的事件,但 mousemove 以非常奇怪的方式触发:只会在 touchmove 时触发一到两次。

所有的鼠标事件都按照平常的方式处理。

现代触摸屏上是否有任何方法可以同时处理鼠标和触摸事件?如果 Firefox 触发一对触摸和鼠标事件,在 Chrome 上 touchmovemousemove 会发生什么?我应该将所有鼠标事件转换为触摸事件还是反过来?我希望找到正确的方法来创建响应式界面。

7个回答

20

您无法提前预测要监听哪些事件(例如,您不知道在页面加载后是否会插入USB触摸屏)。

相反,您应该始终同时侦听触摸事件和鼠标事件,但对您处理的触摸事件调用preventDefault()以防止为它们触发已经过时的鼠标事件。有关详细信息,请参见 http://www.html5rocks.com/en/mobile/touchandmouse/


这是比被接受的答案更干净的解决方案。谢谢! - keithjgrant
谢谢你的回答。确实是非常有帮助和更好的答案! - user1106995
1
这个代码可以运行,但在Chrome浏览器中会出现以下警告:Player.js:52 [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080 - David Callanan

9
你应该检查触摸界面的可用性,并根据情况绑定事件。可以尝试这样做:
(function () {
    if ('ontouchstart' in window) {
        window.Evt = {
            PUSH : 'touchstart',
            MOVE : 'touchmove',
            RELEASE : 'touchend'
        };
    } else {
        window.Evt = {
            PUSH : 'mousedown',
            MOVE : 'mousemove',
            RELEASE : 'mouseup'
        };
    }
}());

// and then...

document.getElementById('mydiv').addEventListener(Evt.PUSH, myStartDragHandler, false);

如果你想同时处理触摸事件和鼠标事件,而浏览器无法将触摸事件良好地转换为鼠标事件,你可以捕获触摸事件并停止它们 - 然后浏览器不会触发相应的鼠标事件(你就不会有重复事件),你可以自己触发它作为鼠标事件或者直接处理它。


var mydiv = document.getElementsById('mydiv');
mydiv.addEventListener('mousemove', myMoveHandler, false);
mydiv.addEventListener('touchmove', function (e) {
    // stop touch event
    e.stopPropagation();
    e.preventDefault();

    // translate to mouse event
    var clkEvt = document.createEvent('MouseEvent');
    clkEvt.initMouseEvent('mousemove', true, true, window, e.detail, 
                 e.touches[0].screenX, e.touches[0].screenY, 
                 e.touches[0].clientX, e.touches[0].clientY, 
                 false, false, false, false, 
                 0, null);
    mydiv.dispatchEvent(clkEvt);

    // or just handle touch event
    myMoveHandler(e);
}, false);

如果连接了触摸屏,我就不处理鼠标。 - johnny
我不会使用第一种方法。阅读更多:https://plus.google.com/+PaulIrish/posts/Y2jydx31Bor - zachleat
2
不幸的是,这种方法在Firefox中不起作用。即使在触摸事件上调用preventDefaultstopPropagation,此浏览器仍会发送鼠标事件...到目前为止,我找到的唯一解决方法是忽略与先前的touchstart事件相同位置开始的mousedown事件,并忽略没有先前mousedownmousemovemouseup事件。很遗憾,其他浏览器不支持Microsoft的Pointer API。它比这个可恶的Touch API更容易使用... - kayahr
1
不幸的是,在我的iPad(iOS7)上,即使使用e.preventDefault()和e.stopPropagation(),我确实有很多情况下两个事件都被触发了 :-( - Garavani
永远不要使用Event.stopPropagation(),除非你真的知道你在做什么。看起来这并不是这种情况。 - Roko C. Buljan
显示剩余2条评论

7

1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community
这不是最佳答案的原因是什么? - Tom

1
我发现这个帖子,因为我有一个类似且更复杂的问题:
假设我们创建了一个js可启用的可滚动区域,其中包含NEXT/PREVIOUS箭头,我们不仅希望它响应触摸和鼠标事件,而且还希望在用户继续按住屏幕或按住鼠标时重复触发它们!
事件的重复会导致我的下一个按钮前进2个位置而不是一个!
借助闭包的帮助,一切似乎都是可能的:
(1)首先为变量隔离创建自调用函数:
 (function(myScroll, $, window, document, undefined){
 ...
 }(window.myScroll = window.myScroll || {}, jQuery, window, document));

(2)然后,添加用于保存setTimeout()内部状态的私有变量:

/*
 * Primary events for handlers that respond to more than one event and devices  
 * that produce more than one, like touch devices.
 * The first event in browser's queue hinders all subsequent for the specific 
 * key intended to be used by a handler.
 * Every key points to an object '{primary: <event type>}'.
 */
var eventLock = {};
// Process ids based on keys.
var pids = {};
// Some defaults
var defaults = {
   pressDelay: 100 // ms between successive calls for continuous press by mouse or touch
}

(3)事件锁定功能:
function getEventLock(evt, key){
   if(typeof(eventLock[key]) == 'undefined'){
      eventLock[key] = {};
      eventLock[key].primary = evt.type;
      return true;
   }
   if(evt.type == eventLock[key].primary)
      return true;
   else
      return false;
}
function primaryEventLock(evt, key){
   eventLock[key].primary = evt.type;
}

(4) 添加事件处理程序:
function init(){
   $('sth').off('mousedown touchstart', previousStart).on('mousedown touchstart', previousStart);
   $('sth').off('mouseup touchend', previousEnd).on('mouseup touchend', previousEnd);
   // similar for 'next*' handlers
}

触发事件mousedowntouchstart将为支持两者的设备上的处理程序产生双重调用(可能是触摸先触发)。mouseuptouchend也是如此。

我们知道输入设备(整个图形环境实际上)按顺序产生事件,因此我们不关心哪个首先触发,只要在从处理程序next*()previous*()捕获的第一个事件中设置了特殊键,即私有eventLock.next.primaryeventLock.previous.primary

是事件类型,因此第二、第三等事件始终是失败者,它们不能通过锁定函数eventLock()primaryEventLock()获取锁。

(5)上述内容可以在事件处理程序的定义中看到:

function previousStart(evt){
   // 'race' condition/repetition between 'mousedown' and 'touchstart'
   if(!getEventLock(evt, 'previous'))
      return;

   // a. !!!you have to implement this!!!
   previous(evt.target);

   // b. emulate successive events of this type
   pids.previous = setTimeout(closure, defaults.pressDelay);

   // internal function repeats steps (a), (b)
   function closure(){
      previous(evt.target);
      primaryEventLock(evt, 'previous');
      pids.previous = setTimeout(closure, defaults.pressDelay);
   }
};
function previousEnd(evt){
      clearTimeout(pids.previous);
};

nextStartnextEnd同理。

思路是,无论是触摸还是鼠标,第一个之后的人都不能通过function eventLock(evt, key)获得锁定并停在那里。

唯一打开此锁定的方法是在步骤(4)中触发终止事件处理程序*End()previousEndnextEnd

我还以非常聪明的方式处理了在会话中间连接的触摸设备的问题:我注意到长按时间超过defaults.pressDelay会产生连续的回调函数调用,仅针对该时刻的主要事件(原因是没有结束事件处理程序终止回调)!

touchstart event
closure
closure
....
touchend event

我将“主要设备”定义为用户正在使用的设备,因此您只需长按即可使用primaryEventLock(evt,'previous')在闭包内立即使您的设备成为主要设备

另外,请注意执行previous(event.target)所需的时间应小于defaults.pressDelay

(6) 最后,让我们将init()暴露给全局范围:

myScroll.init = init;

你应该用手头的问题 fiddle 替换对 previous(event.target) 的调用。
同时,注意到在 (5b) 中有另一个常见问题的解决方案:我们如何将参数传递给从 setTimeout() 调用的函数,即 setTimeout(previous, defaults.pressDelay) 缺乏参数传递机制。

1

鼠标事件和触摸事件在技术上并不完全提供相同的功能,但对于大多数情况而言,它们可以互换使用。此解决方案不偏向其中任何一种,因为用户可能同时拥有鼠标和触摸屏。相反,只要用户在更改输入设备之前等待至少五秒钟,就允许用户使用任何输入设备。当触摸屏被轻敲时,此解决方案忽略了触摸屏设备上的鼠标指针仿真。

var lastEvent = 3  ;
var MOUSE_EVENT = 1;
var TOUCH_EVENT = 2 ; 
 

element.addEventListener('touchstart', function(event)
  {
    
   if (lastEvent === MOUSE_EVENT  )
   {
    var time =  Date.now() - eventTime  ;  
    if ( time > 5000 ) 
    {
     eventTime = Date.now() ;
     lastEvent = TOUCH_EVENT ;
     interactionStart(event) ;
    } 
   }
   else 
   {
    lastEvent = TOUCH_EVENT ; ;
    eventTime = Date.now() ;
    interactionStart(event)  ; 
   }
    
  }) ; 

  element.addEventListener('mousedown', function(event)
  {
   
   if (lastEvent === TOUCH_EVENT  )
   {
    var time =  Date.now() - eventTime  ; 
    if ( time > 5000 ) 
    {
     eventTime = Date.now() ;
     lastEvent = MOUSE_EVENT ;
     interactionStart(event) ;
    } 
   }
   else 
   {
    lastEvent=  MOUSE_EVENT ; 
    eventTime = Date.now() ;
    interactionStart(event)  ; 
   }
  }) ;  

function interactionStart(event) // handle interaction (touch or click ) here.
{...}

这绝不是一个万能解决方案,我使用过几次,并没有发现问题,但公正地说,我通常只是用它来在画布被点击时启动动画,或者提供逻辑将 div 转换为按钮。我把这个代码留给你们使用,找到改进和帮助改进这个代码(如果你没有找到更好的解决方案)。

0

我一直在使用这个jQuery助手来绑定触摸和点击事件。

(function ($) {
$.fn.tclick = function (onclick) {
    this.bind("touchstart", function (e) { onclick.call(this, e); e.stopPropagation(); e.preventDefault(); });
    this.bind("click", function (e) { onclick.call(this, e); });   //substitute mousedown event for exact same result as touchstart         
    return this;
  };
})(jQuery);

0

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