【翻译】为网站添加触摸功能

原文地址:为网站添加触摸功能 | Articles | web.dev

作者:Matt Gaunt Twitter 首页

哔哩哔哩干杯🍻

触摸屏可用于越来越多的设备,从手机到桌面设备,不一而足。当用户选择与界面互动时,您的应用应该以直观的方式响应他们的触摸操作。

响应元素状态

您是否曾经因为触摸或点击过网页上的某个元素而怀疑网站是否真的检测到了该元素?

只需在用户轻触界面的各个部分或与这些部分互动时更改元素的颜色,即可基本确定您的网站在正常运行。这样不仅能减轻用户的失望感,还能让其觉得网站敏捷且响应迅速。

DOM 元素可以继承以下任意状态:默认、聚焦、悬停和活跃。如需针对上述每种状态更改界面,我们需要将样式应用于以下伪类 :hover:focus:active,如下所示:

css 复制代码
.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;
}

试试看

在大多数移动浏览器中,系统会在用户点按某个元素后对其应用悬停和/或聚焦状态。hover hover

请仔细考虑您设置的样式以及用户完成触摸后会看到的外观。

注意 :定位点代码和按钮在不同的浏览器中可能会有不同的行为,因此请假定在有些情况下,系统会保持悬停状态,而在其他情况下则保持聚焦状态。hover hover

禁止显示默认浏览器样式

为不同状态添加样式后,您会注意到,大多数浏览器会根据用户的触摸实现自己的样式。这主要是因为,当移动设备首次发布时,许多网站没有 :active 状态的样式。因此,许多浏览器添加了额外的突出显示颜色或样式来向用户提供反馈。

大多数浏览器会使用 outline CSS 属性在某个元素获得焦点时在其周围显示一个圆环。您可以使用以下代码禁止它:

css 复制代码
.btn:focus {
    outline: 0;

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

Safari 和 Chrome 添加了点按突出显示颜色,可通过 -webkit-tap-highlight-color CSS 属性加以阻止:

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

试试看

Windows Phone 上的 Internet Explorer 也有类似行为,但可通过元标记禁止:

ini 复制代码
<meta name="msapplication-tap-highlight" content="no">

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

-moz-focus-inner 伪类,它会在可触摸元素上添加轮廓,您可以通过设置 border: 0 将其移除。

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

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

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

试试看

注意 :请仅在您具有适用于 :hover:active:focus 的伪类时,禁止上述默认样式!

停用用户选择功能

在创建界面时,在某些情况下,您可能希望用户与您的元素互动,但又想禁止长按界面或在界面上拖动鼠标时选择文本的默认行为。

您可以使用 user-select CSS 属性执行此操作,但请注意,如果用户想要选择元素中的文本,那么对内容进行这种操作可能会extremely令人不快。** 因此,请务必谨慎使用。

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

实现自定义手势

如果您想对网站进行自定义互动和手势,需要注意以下两个主题:

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

在本文中,我们就关注这些主题,它们先是介绍覆盖所有浏览器所需支持的 API,然后介绍如何高效地使用这些事件。

根据您希望手势执行的操作,您可能希望用户一次与一个元素互动,**还是希望他们能够同时与多个元素互动。

注意 :有些用户希望使用键盘输入功能,在触摸屏设备上运行辅助技术的用户可能因手势被辅助技术拦截 / 使用而无法执行手势。

在本文中,我们将介绍两个示例,这两个示例都展示了如何支持所有浏览器以及如何保持较高的帧速率。

第一个示例可让用户与一个元素互动。在这种情况下,您可能希望将所有触摸事件都提供给这一个元素,只要手势最初是从元素本身开始的。例如,将手指从可滑动元素上移开仍然可以控制该元素。

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

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

这对用户而言更为灵活,但会使操纵界面的逻辑复杂化,并且对用户错误的适应性较差。

添加事件监听器

在 Chrome(版本 55 及更高版本)、Internet Explorer 和 Edge 中,建议使用 PointerEvents 方法实现自定义手势。

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

PointerEvents 的一大功能是,它将多种类型的输入(包括鼠标、触摸和触控笔事件)合并到一组回调中。要监听的事件包括 pointerdownpointermovepointeruppointercancel

在其他浏览器中,等效于触摸事件的 touchstarttouchmovetouchendtouchcancel;如果您想为鼠标输入实现相同的手势,则需要实现 mousedownmousemovemouseup

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

如需使用这些事件,需要对 DOM 元素调用 addEventListener() 方法,并调用事件名称、回调函数和布尔值。此布尔值用于确定您应该在其他元素有机会捕获并解读事件之前还是之后捕获事件。(true 表示您希望事件优先于其他元素。)

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

kotlin 复制代码
// 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);
}

试试看

注意 :由于 API 的设计,PointerEvents 只需要一个 pointerdown 事件就可以同时处理鼠标和触摸事件。

处理单元素交互

在上面的简短代码段中,我们仅为鼠标事件添加了起始事件监听器。原因在于,只有当光标悬停在添加了事件监听器的元素上方时,才会触发鼠标事件。**

无论触摸发生在什么位置,TouchEvents 都会在手势开始后对其进行跟踪;在我们对 DOM 元素调用 setPointerCapture 后,PointerEvents无论触摸发生在何处,都将跟踪事件。

对于鼠标移动和结束事件,我们在手势开始方法中添加了事件监听器,并向文档添加了监听器,这意味着它可以跟踪光标,直到手势完成为止。**

实现此目标的步骤如下:

  1. 添加所有 TouchEvent 和 PointerEvent 监听器。对于 MouseEvents,添加开始事件。
  2. 在开始手势回调内,将鼠标移动和结束事件绑定到文档。这样,无论事件是否发生在原始元素上,都可以接收所有鼠标事件。对于 PointerEvents,我们需要对原始元素调用 setPointerCapture() 来接收所有进一步的事件。然后处理手势开始。
  3. 处理移动事件。
  4. 对于结束事件,从文档中移除鼠标移动操作和结束监听器,然后结束手势。

以下是 handleGestureStart() 方法的代码段,它会向文档添加移动和结束事件:

javascript 复制代码
// 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(),它会从文档中移除移动和结束事件监听器,并在手势完成时释放指针捕获,如下所示:

javascript 复制代码
// 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);

试试看

通过遵循向文档添加移动事件的这种模式,如果用户开始与元素互动并将其手势移到元素外部,那么无论鼠标在页面上的什么位置移动,我们都会继续获取鼠标移动事件,因为事件是从文档接收的。

下图显示了在手势开始后我们向文档添加移动和结束事件时触摸事件的作用。

高效响应触摸动作

现在,我们已处理好开始和结束事件,接下来就可以实际响应触摸事件了。

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

以下示例通过检查 targetTouches 是否存在来检查事件是否来自 TouchEvent。如果是,它会从第一次轻触中提取 clientXclientY。如果事件是 PointerEventMouseEvent,它会直接从事件本身提取 clientXclientY

ini 复制代码
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):

javascript 复制代码
// 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() 函数作为回调:

ini 复制代码
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 之前,为要移动的任何元素设置转换,以允许下一个触摸事件请求新的动画帧。

ini 复制代码
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 来阻止浏览器在用户触摸时执行任何操作,以便拦截所有触摸事件。

css 复制代码
/* Pass all touches to javascript: */
button.custom-touch-logic {
  touch-action: none;
}

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

touch-action 可让您停用由浏览器实现的手势。例如,IE10+ 支持点按两次进行缩放的手势。将 touch-action 设置为 manipulation 可以阻止默认的点按两次行为。

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

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

触摸操作参数
touch-action: none 浏览器不会处理任何触摸互动。
touch-action: pinch-zoom 停用除"pinch-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'

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

ini 复制代码
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 更新文章

参考

触摸状态的伪类

示例 说明
:hover 将光标放在元素上方时输入。 悬停时界面的变化有助于鼓励用户与元素互动。
:焦点 当用户按 Tab 键浏览页面上的元素时,系统会输入此错误代码。焦点状态可让用户知道他们当前正在与哪个元素互动;还可以让用户使用键盘轻松浏览您的界面。
:有效 当选择某个元素时(例如,当用户点击或轻触某个元素时)输入此元素。

如需查看明确的触摸事件参考文档,请参阅:W3C 触摸事件

触摸、鼠标和指针事件

以下事件是向应用添加新手势的构建块:

触摸、鼠标、指针事件
touchstartmousedownpointerdown 当手指首次触摸某个元素或用户点击鼠标时,系统会调用此方法。
touchmovemousemovepointermove 当用户在屏幕上移动手指或使用鼠标拖动时,系统会调用此方法。
touchendmouseuppointerup 当用户将手指从屏幕上抬起或松开鼠标时,系统会调用此方法。
touchcancel pointercancel 当浏览器取消触摸手势时,系统会调用此方法。例如,用户轻触某个 Web 应用,然后更改标签页。

触控列表

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

触摸事件属性
touches 屏幕上当前所有触摸的列表(不考虑正在触摸的元素)。
targetTouches 从当前事件的目标元素上开始的触摸列表。例如,如果绑定到 <button>,则只会获得该按钮上的当前触摸。如果您绑定到文档,就可以获取文档当前的所有触控操作。
changedTouches 因发生更改而导致事件触发的触摸列表:- 对于 touchstart 事件 - 随着当前事件刚刚变为活跃的接触点列表。
  • 对于 touchmove 事件 - 自上次事件后移动的接触点列表。
  • 对于 touchendtouchcancel 事件 - 刚刚从界面中移除的接触点列表。 |

在 iOS 上启用活跃状态支持

遗憾的是,iOS 版 Safari 默认不会应用 active 状态。为使它正常运行,您需要向文档正文 或每个元素添加一个 touchstart 事件监听器。

您应在用户代理测试之后执行此操作,以便测试仅在 iOS 设备上运行。

向 body 添加触摸开始的优点是可以应用于 DOM 中的所有元素,但在滚动页面时可能会出现性能问题。

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

另一种方法是将触摸开始监听器添加到页面中的所有可交互元素,从而缓解一些性能问题。

ini 复制代码
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);
    }
  }
};

注:这篇文章是我很久以前在google开发者文档中心看到的一篇关于在web上添加touch功能的文章,文章通俗易懂,示例丰富,代码优美,对我帮助很大,虽然文章是2011年作者写的,但放在今天,依然是很好的文章,所以我只是简单借助工具翻译了一下文章和视频,保证文章、视频可读可访问,示例可以直接访问,原样搬过来的

相关推荐
GISer_Jing2 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪3 小时前
CSS复习
前端·css
咖啡の猫5 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲7 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5818 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路8 小时前
GeoTools 读取影像元数据
前端
ssshooter9 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry9 小时前
Jetpack Compose 中的状态
前端
dae bal10 小时前
关于RSA和AES加密
前端·vue.js
柳杉10 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化