为你的网站添加触控

实现自定义手势

如果你有为你的网站添加自定义交互方式和手势的想法,那么有两点需要谨记,一,如何支持不同种类的移动浏览器;二,如何保持高帧数。在此篇中,我们将窥探一下关于这方面的内容。

用事件来响应触控输入

这取决于你使用触摸做什么,你可能会遇到以下两种状况之一:

  • 我想让用户与某一特定元素进行交互。
  • 我想让用户同时与多种元素进行交互。

这两种交互都有取舍之处。

如果用户将只能够和一个元素进行交互,你可能想:只要手势最初从某个元素开始,就让全部的触摸事件都集中在该元素上。例如:将手指从可滑动的元素上离开时却仍能操控这个元素。

Example GIF of touch on document

然而,如果你期待用户同时与多元素进行交互(使用多点触控),那么你应该对特定元素进行触控限制。

Example GIF of touch on element

关键要点

  • 对于全设备的支持,管理触摸,鼠标以及指针事件。
  • 时刻为元素本身绑定起始监听器。
  • 如果你想让用户与某一特定元素进行交互,那么用touchstar方法为文档绑定你的移动和结束监听器。请确保在结束监听时将他们从文档中解绑。
  • 如果你想支持多点触控,那么你可以将移动和结束的触摸事件绑定到元素本身,或者在一个元素上控制所有触摸。

添加事件监听器

触摸事件和鼠标事件在大多数移动浏览器上能够执行。

你所需要执行的事件名称是touchstart, touchmove, tauchendtouchcancel

在一些情况下,你可能发现你同时想支持鼠标交互,那么你可以使用鼠标事件: mousedownmousemove, 和 mouseup

对于Windows触控设备,你需要支持一系列新事件当中的指针事件。指针事件将鼠标事件和触摸事件合并在了同一回调系列中。目前,指针事件和MSPointerDown事件,MSPointerMove事件,以及MSPointerUp事件只支持Internet Explorer 10和更高版本。

触控,鼠标和指针事件是为你的程序添加手势操作的砖瓦。 (请参考 触摸,鼠标和MS指针事件).

将这些事件及其回调函数与布尔值放置在addEventListener()方法中。这个布尔值决定了其他元素有机会捕获并执行该事件之前或之后,你是否能够捕获这个事件。(真值ture意味着我们想要这个事件放在其他元素之前执行):

    // Check if pointer events are supported.
    if (window.navigator.msPointerEnabled) {
      // Add Pointer Event Listener
      swipeFrontElement.addEventListener('MSPointerDown', this.handleGestureStart, true);
    } else {
      // Add Touch Listener
      swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
    
      // Add Mouse Listener
      swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
    }
    
查看完整示例

这些代码首先使用window.navigator.msPointerEnabled检测是否支持指针事件。若不支持,我们就换用触控事件和鼠标事件监听器。

window.PointerEventSupport的值是检测window.PointerEvent或现已被废弃的window.navigator.msPointerEnabled对象是否存在。如果它支持,我们就可以用事件名的变量,由window.PointerEvent是否存在决定是否使用版本前缀。

控制单个元素交互

在上方简短的几行代码中你可能已经注意到了我们只添加了开始事件监听器,这是我们有意为之。

在触摸手势开始的时候,在元素上添加移动和结束事件监听器,可以让浏览器检测触摸事件是否发生在事件监听器范围之内,倘若不是,也可以通过避免运行额外的javascript来加快运行速度 。

Illustrating Binding Touch Events to Document in touchstart

以下为执行中所需的步骤:

  1. 为某一元素添加开始事件监听器。
  2. 在你的触控开始方法中,为文档绑定移动和结束元素。其原因是我们可以因此无视事件是否发生在原始元素上而接收所有事件。
  3. 控制移动元素。
  4. 在结束事件中,为文件移除移动监听器和结束监听器。

以下是我们handleGestureStart方法的部分代码,其中为文档添加了移动和结束事件:

    // Handle the start of gestures
    this.handleGestureStart = function(evt) {
      evt.preventDefault();

      if(evt.touches && evt.touches.length > 1) {
        return;
      }

      // Add the move and end listeners
      if (window.navigator.msPointerEnabled) {
        // Pointer events are supported.
        document.addEventListener('MSPointerMove', this.handleGestureMove, true);
        document.addEventListener('MSPointerUp', this.handleGestureEnd, true);
      } else {
        // Add Touch Listeners
        document.addEventListener('touchmove', this.handleGestureMove, true);
        document.addEventListener('touchend', this.handleGestureEnd, true);
        document.addEventListener('touchcancel', this.handleGestureEnd, true);
    
        // Add Mouse Listeners
        document.addEventListener('mousemove', this.handleGestureMove, true);
        document.addEventListener('mouseup', this.handleGestureEnd, true);
      }
    
      initialTouchPos = getGesturePointFromEvent(evt);

      swipeFrontElement.style.transition = 'initial';
    }.bind(this);
    
查看完整示例

我们添加的结束回调是handleGestureEnd,它会在手势完成后,从文件中移除移动事件和结束事件:

    // Handle end gestures
    this.handleGestureEnd = function(evt) {
      evt.preventDefault();

      if(evt.touches && evt.touches.length > 0) {
        return;
      }

      isAnimating = false;
    
      // Remove Event Listeners
      if (window.navigator.msPointerEnabled) {
        // Remove Pointer Event Listeners
        document.removeEventListener('MSPointerMove', this.handleGestureMove, true);
        document.removeEventListener('MSPointerUp', this.handleGestureEnd, true);
      } else {
        // Remove Touch Listeners
        document.removeEventListener('touchmove', this.handleGestureMove, true);
        document.removeEventListener('touchend', this.handleGestureEnd, true);
        document.removeEventListener('touchcancel', this.handleGestureEnd, true);
    
        // Remove Mouse Listeners
        document.removeEventListener('mousemove', this.handleGestureMove, true);
        document.removeEventListener('mouseup', this.handleGestureEnd, true);
      }
    
      updateSwipeRestPosition();
    }.bind(this);
    
查看完整示例

鼠标事件的模式也是一样的,因为对用户来说,他们偶尔非常容易点击非元素所在的地方。这导致了移动事件失效。通过为文档添加移动事件,不管在界面的何处,我们都可以持续获得鼠标移动的信息。

你可以在Chrome DevTools中使用"Show potential scroll bottlenecks"功能,展示触摸事件的工作状态:

Enable Scroll Bottleneck in DevTools

通过这个方法,你可以看到触摸事件绑定到了哪,并确认你的添加与移除监听器逻辑是否如你所愿地工作。

控制多元素交互

如果你希望你的用户一次使用多个元素,你可以直接为元素添加移动事件监听器和结束事件监听器。这只应用于触控,因为你应该继续将mousemove以及mouseup监听器应用于文档。

因为我们只希望追踪某一特定元素上的触控,所以我们能够马上为元素的触摸及指针事件添加移动和结束监听器:

    // Check if pointer events are supported.
    if (window.navigator.msPointerEnabled) {
      // Add Pointer Event Listener
      elementHold.addEventListener('MSPointerDown', this.handleGestureStart, true);
      elementHold.addEventListener('MSPointerMove', this.handleGestureMove, true);
      elementHold.addEventListener('MSPointerUp', this.handleGestureEnd, true);
    } else {
      // Add Touch Listeners
      elementHold.addEventListener('touchstart', this.handleGestureStart, true);
      elementHold.addEventListener('touchmove', this.handleGestureMove, true);
      elementHold.addEventListener('touchend', this.handleGestureEnd, true);
      elementHold.addEventListener('touchcancel', this.handleGestureEnd, true);

      // Add Mouse Listeners
      elementHold.addEventListener('mousedown', this.handleGestureStart, true);
    }
    
查看完整示例

在我们的handleGestureStarthandleGestureEnd函数中,我们分别为文档添加和移除了鼠标事件监听器。

    // Handle the start of gestures
    this.handleGestureStart = function(evt) {
      evt.preventDefault();

              var point = getGesturePointFromEvent(evt);
      initialYPos = point.y;
    
      if (!window.navigator.msPointerEnabled) {
        // Add Mouse Listeners
        document.addEventListener('mousemove', this.handleGestureMove, true);
        document.addEventListener('mouseup', this.handleGestureEnd, true);
      }
    }.bind(this);

    this.handleGestureEnd = function(evt) {
      evt.preventDefault();
    
      if(evt.targetTouches && evt.targetTouches.length > 0) {
        return;
      }
    
      if (!window.navigator.msPointerEnabled) {
        // Remove Mouse Listeners
        document.removeEventListener('mousemove', this.handleGestureMove, true);
        document.removeEventListener('mouseup', this.handleGestureEnd, true);
      }

      isAnimating = false;
      lastHolderPos = lastHolderPos + -(initialYPos - lastYPos);
    }.bind(this);
    
查看完整示例

使用触摸时是60fps

现在,我们拥有了所关心的开始事件和结束事件,因此我们能够真正地给予触摸事件以回应。

获取并储存触摸事件坐标

对于任何一个开始事件和移动事件,你都可以轻松从某一事件中提取xy

以下的代码片段通过查询targetTouches来检查事件是否来自触摸事件。如果是,稍后会从首次触摸中提取clientXclientY。如果事件是一个鼠标事件或者是指针事件,稍后我们就直接从事件本身里提取clientXclientY

    function getGesturePointFromEvent(evt) {
        var point = {};

        if(evt.targetTouches) {
          // Prefer Touch Events
          point.x = evt.targetTouches[0].clientX;
          point.y = evt.targetTouches[0].clientY;
        } else {
          // Either Mouse event or Pointer Event
          point.x = evt.clientX;
          point.y = evt.clientY;
        }

        return point;
      }
    
查看完整示例

所有的触摸事件的触摸数据中都有三个列表 (亦可参考 [触摸列表](#touch-lists)):

  • touches:屏幕上当前所有触摸的列表,忽略正在运行的DOM元素。
  • targetTouches:当前在被绑定了事件的DOM元素上的触摸事件列表。
  • changedTouches:发生改变并导致事件处于激活状态的触摸事件列表。

在大部分情况下,targetTouches可以提供你所需要的一切。

请求动画帧数

因为事件回调在主线程中被激活,我们会想要在回调中尽可能地运行一部分代码,以保持框架的高帧率并防止遗漏。

使用requestAnimationFrame来改变用户界面以响应某一事件。在浏览器计划构建一个框架的时候,这给予你一次升级用户界面的机会。并会帮助你解决一些回调问题。

典型的执行步骤是从开始事件和移动事件中保存xy的坐标,然后在移动事件回调中获取一个动画帧animation frame。

在我们的演示中,我们将初始触摸位置保存在handleGestureStart中:

    // Handle the start of gestures
    this.handleGestureStart = function(evt) {
      evt.preventDefault();

      if(evt.touches && evt.touches.length > 1) {
        return;
      }

      // Add the move and end listeners
      if (window.navigator.msPointerEnabled) {
        // Pointer events are supported.
        document.addEventListener('MSPointerMove', this.handleGestureMove, true);
        document.addEventListener('MSPointerUp', this.handleGestureEnd, true);
      } else {
        // Add Touch Listeners
        document.addEventListener('touchmove', this.handleGestureMove, true);
        document.addEventListener('touchend', this.handleGestureEnd, true);
        document.addEventListener('touchcancel', this.handleGestureEnd, true);
    
        // Add Mouse Listeners
        document.addEventListener('mousemove', this.handleGestureMove, true);
        document.addEventListener('mouseup', this.handleGestureEnd, true);
      }
    
      initialTouchPos = getGesturePointFromEvent(evt);

      swipeFrontElement.style.transition = 'initial';
    }.bind(this);
    
查看完整示例

如果我们需要的话,handleGestureMove方法可以在获取动画框架前保存y的位置, 并作为回调传送给我们的onAnimframe函数:

    var point = getGesturePointFromEvent(evt);
    lastYPos = point.y;
    
      if(isAnimating) {
        return;
      }

      isAnimating = true;
      window.requestAnimFrame(onAnimFrame);
    
查看完整示例

onAnimFrame函数中,我们得以改变用户界面以移动元素。首先,我们要检查手势是否任在运行,再决定我们是否要添加动画特效。如果是那样,我们使用y的初始位置和结束位置来为我们的元素计算新的变动。

一旦完成变动,我们就将isAnimating赋值为fasle,这样一来,下一个的触控事件就要获取一个新的动画帧。

    function onAnimFrame() {
        if(!isAnimating) {
          return;
        }
    
        var newYTransform = lastHolderPos + -(initialYPos - lastYPos);
    
        newYTransform = limitValueToSlider(newYTransform);
    
        var transformStyle = 'translateY('+newYTransform+'px)';
        elementHold.style.msTransform = transformStyle;
        elementHold.style.MozTransform = transformStyle;
        elementHold.style.webkitTransform = transformStyle;
        elementHold.style.transform = transformStyle;
    
        isAnimating = false;
      }
    
查看完整示例

使用触摸动作控制卷动

CSS属性中的touch-action允许你在触控时控制滚动操作。在我们的例样中,我们使用touch-action: none以禁用触摸时的滚动操作。

    /* Pass all touches to javascript */
    -ms-touch-action: none;
    touch-action: none;
    
查看完整示例

touch-action允许浏览器禁用手势,比如IE10+支持双击缩放,并通过设置pan-x | pan-y | manipulation触摸动作防止你的双击行为。

这样做的好处是,它可以允许你自己实现这些手势,但是在IE10+的这种情况下,你也移除了200毫秒的点击延迟。

以下是touch-action可用的参数列表。

属性 描述
touch-action: auto 浏览器会根据其支持的触控添加交互。比如x轴滚动、y轴滚动、双指缩放和双击。
touch-action: none 浏览器禁用触摸交互。
touch-action: pan-x 允许浏览器水平滚动,禁用垂直滚动及手势。
touch-action: pan-y 允许浏览器垂直滚动,禁用水平滚动及手势。
touch-action: manipulation 允许浏览器双方向滚动,也允许双指缩放;但忽略其他手势。

谨记

  • 使用 touch-action: pan-x 或者 touch-action: pan-y 能够很明确地将你的意图传递给用户,即用户应只在一个元素上垂直或者水平滚动。

参考

权威的触摸事件参考条目可以在这里找到: w3触摸事件.

触摸,鼠标和MS指针事件

这些事件是为你的程序添加新的手势操作的砖瓦:

触摸,鼠标和MS指针事件 描述
touchstart, mousedown, MSPointerDown 在单指首次触摸元素或者当用户点击鼠标时被触发。
touchmove, mousemove, MSPointerMove 用户在屏幕上移动手指或者使用鼠标拖拽时被触发。
touchend, mouseup, MSPointerUp 在用户停止触摸和释放鼠标时被触发。
touchcancel 在浏览器撤销触摸手势时被触发。

触摸列表

每个触摸事件包括三个列表属性:

属性 描述
touches 目前屏幕上所有触控的列表,无论元素是否被触摸。
targetTouches 当前事件的目标元素,自该事件开始所接收的所有触控行为的列表。例如:如果你绑定一个<button>,你将只会得到当前在那个按键上的触控。如果你绑定一个文档,你将会得到当前在那个文档上的所有触控。
changedTouches 发生改变并导致事件处于激活状态的触摸事件列表。
  • 对于touchstart 事件-- 当前事件刚激活的触控点列表。
  • 对于touchmove 事件-- 上次事件以来已经发生移动的触控点列表。
  • 对于 touchend 事件和touchcancel 事件-- 界面上已被移除的触控点。

除非另有说明,本网页的内容采用知识共享署名3.0许可和代码示例都基于Apache2.0许可。如需详细资讯,请参阅我们的网站政策

回到顶部