为网站添加触摸功能

从手机到桌面设备的屏幕,使用触摸屏的设备越来越多。您的应用应以直观且美观的方式响应用户触摸动作。

Matt Gaunt

触摸屏适用于越来越多的设备,从手机到 桌面设备屏幕当用户选择与您的界面进行交互时,您的应用 应以直观的方式响应用户触摸动作。

响应元素状态

您是否曾经有过这样的经历:触摸或点击网页上的某个元素, 该网站是否真的检测到它?

只需在用户触摸零部件或与其互动时改变元素的颜色 基本上保证您的网站正常运行。不仅 这样既能减轻用户的失望感,又能使图片呈现简洁明快、响应迅速的感觉

DOM 元素可继承以下任意状态:default、focus、hover 有效。若要针对以上每种状态更改界面,我们需要应用样式 添加到以下伪类 :hover:focus:active,如下所示:

.btn {
  background-color: #4285f4;
}

.btn:hover {
  background-color: #296cdb;
}

.btn:focus {
  background-color: #0f52c1;

  /* The outline parameter suppresses the border
  color / outline when focused */
  outline: 0;
}

.btn:active {
  background-color: #0039a8;
}

试试看

显示不同颜色的按钮的图片
州

在大多数移动浏览器中,悬停和/或焦点状态会应用于元素 并在其被点按后显示在屏幕上

仔细考虑您设置的样式,以及用户在使用更改后的样式 完成相应操作

禁止使用默认浏览器样式

为不同状态添加样式后,您会注意到 实现自己的样式来响应用户触摸。这在很大程度上 因为当移动设备首次发布时,许多网站 具有 :active 状态的样式。因此,许多浏览器 提供额外的突出显示颜色或样式,以向用户提供反馈。

大多数浏览器使用 outline CSS 属性在 元素。您可以使用以下命令禁止该消息:

.btn:focus {
    outline: 0;

    /* Add replacement focus styling here (i.e. border) */
}

Safari 和 Chrome 增加了点按突出显示颜色,使用 -webkit-tap-highlight-color CSS 属性:

/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
  -webkit-tap-highlight-color: transparent;
}

试试看

Windows Phone 上的 Internet Explorer 也有类似行为,但会被禁用 :

<meta name="msapplication-tap-highlight" content="no">

Firefox 有两个副作用需要处理。

-moz-focus-inner 伪类,会在 可触摸元素,可通过设置 border: 0 来移除。

如果您在 Firefox 上使用 <button> 元素,则会获得渐变效果 已应用,您可以通过设置 background-image: none 将其移除。

/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
  background-image: none;
}

.btn::-moz-focus-inner {
  border: 0;
}

试试看

停用用户选择功能

在创建界面时,您可能希望用户 但您希望禁止默认行为 就是通过长按或拖动鼠标在界面上选择文本

您可以使用user-select CSS 属性来实现此目的,但请注意, 对内容做这类事情可能极其令人抓狂 如果用户想选择元素中的文字,则可以为其指定此属性。 因此,请务必谨慎使用。

/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
  user-select: none;
}

实现自定义手势

如果您想到了一个网站自定义互动和手势建议, 需要注意两个主题:

  1. 如何支持所有浏览器。
  2. 如何保持较高的帧速率。

在本文中,我们将具体探讨这些主题,涵盖 然后介绍我们如何使用这些事件 。

根据您希望手势执行的操作,您可能需要使用 用户一次只与一个元素进行互动或者您希望他们能够 可以同时与多个元素互动

在本文中,我们将通过两个示例来介绍 以及如何保持较高的帧速率。

文档触摸 GIF 示例

第一个示例允许用户与某个元素互动。在本课中, 那么您可能希望将所有触摸事件都提供给这一个元素, 手势最初是在元素本身上开始的。例如,将一个 手指离开可滑动元素时,仍然可以控制该元素。

这种做法非常有用,因为它能为用户提供极大的灵活性, 会对用户与界面的交互方式施加限制。

触摸元素 GIF 示例

不过,如果您希望用户能同时与多个元素互动 时间(使用多点触控),则应将触控范围限制在特定的 元素。

这对用户来说更灵活,但会使操作的逻辑复杂化 并且对用户错误的适应能力较差。

添加事件监听器

在 Chrome(版本 55 及更高版本)、Internet Explorer 和Edge、 PointerEvents 是实现自定义手势的推荐方法。

在其他浏览器中,TouchEventsMouseEvents 是正确的方法。

PointerEvents 的一大特色是,它可以合并多种类型的输入, 包括鼠标、触摸和笔事件, 回调。要监听的事件包括 pointerdownpointermovepointeruppointercancel

在其他浏览器中,等效项是 touchstarttouchmovetouchendtouchcancel,用于触摸事件;如果您想要实现 与实现 mousedown 所需的鼠标输入相同的手势, mousemovemouseup

如果您对要使用哪些事件有疑问,请查看此表格, 触摸、鼠标和指针事件

使用这些事件需要对 DOM 调用 addEventListener() 方法 元素,以及事件名称、回调函数和布尔值。 这个布尔值决定了您要在事件之前还是之后捕获事件 其他元素则有机会捕捉并解读 事件。(true 表示您希望该事件排在其他元素之前。)

下面是一个监听互动开始的示例。

// Check if pointer events are supported.
if (window.PointerEvent) {
  // Add Pointer Event Listener
  swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
  // Add Touch Listener
  swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);

  // Add Mouse Listener
  swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}

试试看

处理单元素交互

在上面的简短代码段中,我们只添加了起始事件监听器 鼠标事件。这样做的原因是,鼠标事件只会 。

无论手势在哪里,TouchEvents 都会在手势启动后跟踪手势 发生触摸事件,并且无论触摸事件发生在何处,PointerEvents 都会跟踪事件 发生在对 DOM 元素调用 setPointerCapture 之后。

对于鼠标移动和结束事件,我们 手势启动方法,并将监听器添加到文档中,这意味着 直至手势完成。

实现此功能的步骤如下:

  1. 添加所有 TouchEvent 和 PointerEvent 监听器。对于 MouseEvents,添加 启动事件
  2. 在开始手势回调内,将鼠标移动和结束事件绑定到 文档。这样便可收到所有鼠标事件 事件是否发生在原始元素上。对于 PointerEvents, 需要对原始元素调用 setPointerCapture() 才能接收 所有后续事件然后处理手势的开头。
  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.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // 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;
  }

  rafPending = false;

  // Remove Event Listeners
  if (window.PointerEvent) {
    evt.target.releasePointerCapture(evt.pointerId);
  } else {
    // Remove Mouse Listeners
    document.removeEventListener('mousemove', this.handleGestureMove, true);
    document.removeEventListener('mouseup', this.handleGestureEnd, true);
  }

  updateSwipeRestPosition();

  initialTouchPos = null;
}.bind(this);

试试看

按照上述将移动事件添加到文档的方式,如果 用户开始与某个元素互动,并将手势移至 那么无论鼠标指针位于何处,我们都会继续获知 ,因为事件是从文档接收的。

此图显示了我们添加 移动和结束事件到文档。

说明将触摸事件与文档绑定的示意图
`touchstart`

高效响应轻触操作

现在我们已经完成了开始和结束事件 响应触摸事件。

对于任何开始和移动事件,您都可以轻松提取 xy 事件。

以下示例会按以下方法检查事件是否来自 TouchEvent: 检查 targetTouches 是否存在。如果包含,则会提取 clientXclientY。 如果事件是 PointerEventMouseEvent,则提取 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;
  }

试试看

TouchEvent 有三个包含触摸数据的列表:

  • touches:屏幕上所有当前轻触操作的列表,无论 其所在的 DOM 元素。
  • targetTouches:事件中当前 DOM 元素上的触摸列表 目标
  • changedTouches:因发生变化而导致事件的触摸列表 触发。

在大多数情况下,targetTouches 可以满足您的所有需求。( 请参阅触摸列表)。

使用 requestAnimationFrame

由于事件回调是在主线程上触发的,因此我们希望以 尽可能减少事件回调中的代码, 从而防止出现卡顿

使用 requestAnimationFrame(),我们只需更新界面, 就可以帮助我们将一些 无需处理事件回调

如果您不熟悉 requestAnimationFrame(),可以 可以点击此处了解详情

典型的实现是保存 xy 坐标 启动和移动事件,并请求移动事件内的动画帧 回调。

在演示中,我们将初始触摸位置存储在 handleGestureStart() 中(查找 initialTouchPos):

// 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.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // 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() 方法会存储其事件的位置 在请求动画帧之前,传递我们的 onAnimFrame() 函数作为回调:

this.handleGestureMove = function (evt) {
  evt.preventDefault();

  if (!initialTouchPos) {
    return;
  }

  lastTouchPos = getGesturePointFromEvent(evt);

  if (rafPending) {
    return;
  }

  rafPending = true;

  window.requestAnimFrame(onAnimFrame);
}.bind(this);

onAnimFrame 值是一个函数,被调用时会更改界面。 来移动它将此函数传递到 requestAnimationFrame() 后, 告知浏览器在即将更新网页之前进行调用 (即绘制对页面所做的任何更改)。

handleGestureMove() 回调中,我们最初检查 rafPending 是否为 false, 指示 requestAnimationFrame() 是否已调用 onAnimFrame() 自上次移动事件以来。这意味着我们只有一个 requestAnimationFrame() 在任一时间等待运行

执行 onAnimFrame() 回调时,我们会在任何 在将 rafPending 更新为 false 之前要移动的元素,以允许 请求新动画帧的下一个触摸事件。

function onAnimFrame() {
  if (!rafPending) {
    return;
  }

  var differenceInX = initialTouchPos.x - lastTouchPos.x;
  var newXTransform = (currentXPosition - differenceInX)+'px';
  var transformStyle = 'translateX('+newXTransform+')';

  swipeFrontElement.style.webkitTransform = transformStyle;
  swipeFrontElement.style.MozTransform = transformStyle;
  swipeFrontElement.style.msTransform = transformStyle;
  swipeFrontElement.style.transform = transformStyle;

  rafPending = false;
}

使用触摸操作控制手势

通过 CSS 属性 touch-action,您可以控制默认轻触操作 元素的行为方式。在我们的示例中,我们使用 touch-action: none 来 阻止浏览器对用户的这使得我们能够 来拦截所有触摸事件。

/* Pass all touches to javascript: */
button.custom-touch-logic {
  touch-action: none;
}

使用 touch-action: none 在一定程度上是一种核选项,因为它会阻止所有 默认浏览器行为在许多情况下 是一种更好的解决方案。

touch-action 可让您停用浏览器实现的手势。 例如,IE10 及更高版本支持点按两次进行缩放手势。通过将 您阻止了默认的点按两次,共 manipulation 项,其中 touch-action 项 行为

这样,您就可以自行实现点按两次手势。

下面列出了常用的 touch-action 值:

触摸操作参数
touch-action: none 系统不会处理任何触摸交互 。
touch-action: pinch-zoom 禁止所有浏览器互动,例如 除“punch-zoom”外,“touch-action: none”仍由 。
touch-action: pan-y pinch-zoom 在不使用 JavaScript 的情况下处理水平滚动 停用垂直滚动或双指张合缩放功能(例如图片轮播)。
touch-action: manipulation 停用了点按两次手势 点击延迟。将滚动和双指张合缩放操作留到 。

支持旧版 IE

如果您想支持 IE10,则需要处理带供应商前缀的 PointerEvents

要确认是否支持 PointerEvents,您通常需要查找 window.PointerEvent,但在 IE10 中,您应查找 window.navigator.msPointerEnabled

带供应商前缀的事件名称为:'MSPointerDown''MSPointerUp''MSPointerMove'

以下示例展示了如何检查支持情况并切换 事件名称。

var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';

if (window.navigator.msPointerEnabled) {
  pointerDownName = 'MSPointerDown';
  pointerUpName = 'MSPointerUp';
  pointerMoveName = 'MSPointerMove';
}

// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
  window.PointerEventsSupport = true;
}

有关详情,请参阅这篇于 Microsoft

参考

用于触摸状态的伪类

示例 说明
:悬停
按钮处于按下状态
将光标悬停在元素上时输入。 悬停鼠标时界面的更改有助于鼓励用户进行互动 元素。
:聚焦
处于焦点状态的按钮
当用户按 Tab 键浏览网页上的元素时输入。焦点状态 让用户能够了解他们当前正在与哪个元素互动 ;也可让用户使用键盘轻松浏览您的界面。
:活动
按钮处于按下状态
选择元素时输入,对于 例如,当用户点击或触摸某个元素时。

权威触摸事件参考文档可在以下位置找到: W3C Touch Events

触摸、鼠标和指针事件

这些事件是将新手势添加到 应用:

触摸、鼠标、指针事件
touchstart, mousedown, pointerdown 当手指首次触摸某个元素或 。
touchmove, mousemove, pointermove 当用户在屏幕上移动手指或 使用鼠标执行拖动操作
touchend, mouseup, pointerup 当用户将手指从屏幕上移开时,系统会调用此方法 或松开鼠标。
touchcancel pointercancel 当浏览器取消触摸手势时,将调用此方法。例如: 用户触摸某个 Web 应用,然后更改标签页。

触摸列表

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

触摸事件属性
touches 屏幕上所有当前触摸(无论元素如何)的列表 以及被触摸的对象
targetTouches 从目标元素开始的触摸列表 当前事件例如,如果您绑定到 <button>, 则只能触摸到当前按钮。如果您绑定到 即可看到当前对该文档的所有修改
changedTouches 因发生更改而导致事件触发的触摸列表:
  • 适合 touchstart 事件,即刚刚通过该转化操作变为有效的接触点的列表。 当前事件
  • 适合 touchmove 事件:自上次上次访问后发生过移动的接触点的列表。 事件。
  • 适合 touchend touchcancel 事件,即刚刚移除的接触点的列表 从表面开始。

在 iOS 上启用活跃状态支持

遗憾的是,默认情况下,iOS 版 Safari 不会将 active 状态应用于 要使其正常运行,您需要将 touchstart 事件监听器添加到文档 正文或每个元素。

此操作应在用户代理测试之后进行,以便它只能在 iOS 设备上运行。

向正文添加触摸起始点的优势在于,该点触点可应用于所有元素 ,但在滚动页面时可能会出现性能问题。

window.onload = function() {
  if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
    document.body.addEventListener('touchstart', function() {}, false);
  }
};

另一种方法是向所有可交互操作添加触摸启动监听器 元素,从而缓解一些性能问题。

window.onload = function() {
  if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
    var elements = document.querySelectorAll('button');
    var emptyFunction = function() {};

    for (var i = 0; i < elements.length; i++) {
        elements[i].addEventListener('touchstart', emptyFunction, false);
    }
  }
};