MZhou's blog

让你的站点支持触屏

从手机到桌面屏幕,越来越多的设备拥有了触摸屏。当用户使用你的界面时,你的app应该直观且优雅的支持触摸操作。

让页面元素响应触摸状态

从手机到桌面屏幕,越来越多的设备拥有了触摸屏。当用户使用你的界面时,你的app应该直观且优雅的支持触摸操作。

###添加触摸状态的样式

你有没有触摸或点击一个页面上的元素,有没有奇怪为什么这个站点的页面元素可以检测这些状态?

当用户触摸你的界面中的元素时,仅仅是简单地修改元素的颜色就可以让用户感知你的站点可以正常工作。不只是这个作用,还可以让用户感受到页面的即时响应。

####使用伪类来切换不同触摸状态下的样式

最快地支持触摸的方式就是在页面元素的触摸状态切换时修改其样式。

关键点:

DOM元素可以处于以下四种状态:默认、focus、hover和active。如果你想要改变这些状态下的UI,我们需要使用这三种伪类::hover:focus:active。例如:

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

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

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

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

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

查看样例

看下伪类所对应的触摸状态

###覆盖浏览器默认的触摸状态样式

不同的浏览器实现了它们自己特有的触摸状态样式。当你想要实现自己的样式时,就需要同时覆盖掉浏览器的默认样式。

记住:

####覆盖Tap高亮样式

当移动设备刚出现的时候,很多站点都没有激活状态下的样式。结果很多浏览器在用户触摸浏览器的时候添加了高亮颜色或是其他样式。

Safari和Chrome添加了一个高亮颜色作为Tap高亮样式。这可以通过设置CSS样式-webkit-tap-highlight-color来修改默认样式。

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

Windows Phone上的Internet Explorer有一个类似的行为,但是它需要meta标签来重写:

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

####重写FirefoxOS按钮的状态样式

Firefox的伪类-moz-focus-inner为每个可触摸元素默认添加了一个outline样式。你可以通过设置border:0来移除outline样式。

如果你使用了<button>元素,FirefoxOS会其默认添加个渐变的背景。你可以通过设置background-image:none来覆盖这样式。

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

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

####重写Focus状态下的ouline样式

使用outline:0可以覆盖focused元素的outline颜色。

.btn:focus {
  outline: 0;

  // Add replacement focus styling here
}

#####禁用可触摸UI的user-select功能

在一些移动浏览器上,用户长按屏幕就可以选择文字。但当用户不小心按一个按钮时间太长,并不会触发点击事件而会触发按钮文字的选择,这并不是好的用户体验。

-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;

记住:

###引用

触摸状态的伪类。

实现自定义手势

如果你想要为自己的站点实现一个自定义的交互和手势,那么有两点要记住:要支持哪些移动浏览器和如何保持高帧率。在这篇文章中我们讲一探究竟。

###使用事件来响应触摸操作 根据你想要实现的touch操作,你就需要在如下阵营中选其一:

两者必选其一。

如果用户只需要和一个元素交互,那么只要手势操作开始,你可能就需要把所有的touch事件放在那个元素上。例如,在其他元素上滑动也可以控制要移动的元素。

然后,如果你期望用户与多个元素在同一时间交互,你应该将touch操作限制到特定的元素上。

TL;DR

####添加事件监听器 大多数移动浏览器都实现了Touch事件和鼠标事件。

你需要绑定的事件名是:touchstarttouchmovetouchendtouchcancel

在某些情况下,你可能也需要支持鼠标的交互;那么你可以使用这些事件:mousedownmousemovemouseup

对与Windows Phone的设备,你需要支持一系列Pointer Events。Pointer Events是鼠标和touch事件的合集。目前这只在IE 10+上支持,事件名分别是MSPointerDownMSPointerMoveMSPointerUp

Touch、鼠标和Pointer Events是在你的应用中增加手势操作的重要基础。(查看下Touch、鼠标和Pointer Events

使用addEventListener()方法可以注册这些事件,同时还要传递回调函数和一个布尔值。这个布尔值决定了是否使用捕获模式。为true时表示使用捕获模式时,你可以在其他元素之前捕获或打断事件。

// 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来判断是否支持Pointer Events。如果不支持就在添加touch和鼠标事件的监控。

####处理单个元素的交互 你可能已经注意到,在上面的代码片段中只是添加了开始手势的事件。这是故意这么写的。

一旦手势操作在元素上开始,就添加移动和结束的事件监听器。这样浏览器可以通过touch事件监听器来检查touch操作是否发生了。并且处理地更快,因为在平时(手势操作开始前)不需要运行额外的javascript。

实现的步骤如下:

  1. 添加开始事件监听器到指定元素上;
  2. 在开始事件的监听器中,绑定移动和结束事件的监听器到document上。之所以要绑定在document上,是因为我们需要监控所有的事件,不仅仅是那个指定的元素;(译者注:用户的手势操作有时很快,可能会超出指定的元素)
  3. 处理移动事件;
  4. 在结束事件的监听器中,移除移动和结束事件的监听器;

如下是handleGestureStart方法的代码片段,它添加了移动和结束的事件监听器到document上:

// 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);

鼠标事件也使用相同的处理方法,因为用户的鼠标很可能会不小心移动到指定元素的外面。如果只是将移动事件绑定在元素上,那么很容易会不触发事件。相反地如果绑定在document上面,移动事件将继续触发不论鼠标在页面的哪个地方。

你可以使用Chrome DevTool中的“Show potential scroll bottlenecks”功能来了解touch事件的实现:

####处理多元素的交互

如果你期望用户在同一时间与多个页面元素交互,你可以将对应的移动和结束事件直接绑定到那些元素上。但是这只适用于touch事件。对于鼠标事件,你依旧需要将mousemovemouseup事件绑定到document上面。

如果我们只想要监控特定元素上的touch操作,那么我们可以把touch和pinter事件的移动和结束监听器直接绑定在元素上:

// 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函数中,添加和移除鼠标事件的监听器到document上。

// 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);

###Touch操作时保持60fps 现在我们已经处理好开始和结束事件,我们就可以真正地实现touch事件了。

####获取和存储Touch事件的坐标 对于任何开始和移动事件,你可以轻松地提取出xy坐标。

如下代码片段中通过targetTouches来判断是否是一个touch事件对象。如果事件对象是鼠标或者pointer事件,那么直接获取事件对象的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事件都有三种TouchList属性(见touch列表属性):

大多数情况下targetTouches属性就足够了。

####Request Animation Frame 因为事件回调函数在主线程中触发,我们就需要让回调的运行时间尽量短,以保持高帧率,并且避免卡顿。

在事件回调中使用requestAnimationFrame来修改UI。它可以让你可以在浏览器绘制一帧时更新UI,也可以帮你把一些操作放在回调函数外面。

一个典型的实现在开始和移动事件中把xy坐标保存下来。然后在移动事件的回调函数中调用requestAnimationFrame

在我们的DEMO中,我们在handleGestureStart中存储touch的初始化位置:

// 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方法中,如果需要则会在requestAnimationFrame之前存储y位置,然后将onAnimFrame函数传递作为回调函数:

var point = getGesturePointFromEvent(evt);
lastYPos = point.y;

  if(isAnimating) {
    return;
  }

  isAnimating = true;
  window.requestAnimFrame(onAnimFrame);

onAnimFrame函数中,我们修改UI来移动元素。一开始我们先检查手势是否还在进行,来决定是否继续执行动画。如果需要执行动画,你们我们先计算出新的transform值。

一旦我们设置好transform,我们就将isAnimating变量设置为false,这样下一次touch事件中可以执行新的requestAnimationFrame

 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;
}

####使用touch-action来控制滚动 CSS属性touch-action允许你在触摸时控制滚动行为。在我们的例子中,使用touch-action: none来禁用触摸滚屏功能。

/* Pass all touches to javascript */
-ms-touch-action: none;
touch-action: none;

如下是touch-action的所有可能值。

记住:

  1. 使用touch-action: pan-xtouch-action: pan-y更好,因为你的目的明确,用户只能在元素上水平或垂直的滚动。

###引用 touch事件的标准定义可以通过w3 Touch Event来获取。

####Touch事件、鼠标事件和MS Pointer事件 这些事件是在你的应用中增加手势操作的重要基础。

####Touch list对象 每个touch事件对象都包含三种touch list属性:

MZhou's blog - Taste of life.

zmmbreeze / @zhoumm