home > webfront > SGML > html5 >

移动端的touch事件处理

author:zhoulujun@live.cn    hits:

简要的探讨一下移动端 touch 事件处理几个坑,以及相应的简单处理方法。

  在iPhone 3Gs发布的时候,其自带的移动Safari浏览器就提供了一些与触摸(touch)操作相关的新事件。随后,Android上的浏览器也实现了相同的事件。触摸事件(touch)会在用户手指放在屏幕上面的时候、在屏幕上滑动的时候或者是从屏幕上移开的时候出发。下面具体说明:

  touchstart事件:当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发。


  touchmove事件:当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。


  touchend事件:当手指从屏幕上离开的时候触发。


  touchcancel事件:当系统停止跟踪触摸的时候触发。关于这个事件的确切出发时间,文档中并没有具体说明,咱们只能去猜测了。


触摸事件还包含下面三个用于跟踪触摸的属性

       touches:表示当前跟踪的触摸操作的touch对象的数组。

  targetTouches:特定于事件目标的Touch对象的数组。

  changeTouches:表示自上次触摸以来发生了什么改变的Touch对象的数组。

每个Touch对象包含的属性如下。

  clientX:触摸目标在视口中的x坐标。

  clientY:触摸目标在视口中的y坐标。

  identifier:标识触摸的唯一ID。

  pageX:触摸目标在页面中的x坐标。

  pageY:触摸目标在页面中的y坐标。  

       screenX:触摸目标在屏幕中的x坐标。

screenY:触摸目标在屏幕中的y坐标。

  target:触目的DOM节点目标。

这里,除了前三种changedTouchestargetTouchestouches之外的其他属性,都是我们常见的一些属性值,所以这里对于这些属性就不做处理,而这三个新的属性,是只针对touch事件存在的属性值,并且是我们之后处理时,获取一些关键数据的属性,所以这里就只对这三个属性进行说明。

TouchList

看了上面的列表中的内容,首先先注意到的一点就是,TouchList对象,一个新的也是唯有touch事件的event对象中,才会出现的一种对象,在了解changedTouchestargetTouchestouches这三种属性之前,先对TouchList对象进行一个简单的了解。

TouchList是一个只读的类数组对象,它表示在当前的touch事件中,与触摸屏的接触点的个数,比如:如果你当前是三根手指在同时在触摸屏上,那么每一根手指都会有一个相对应的touch对象,来记录对应手指的操作相关的信息,这个时候的TouchList类数组中,就有三个数据,分别是这三根手指所对应的touch对象,而TouchList类数组的长度也就是3.

根据我们对类数组的了解,既然是类数组,那么就会有length属性,来表示这个类数组的长度,并且可以使用属性的中括号取值方式,取出对应位置的值。OK,下面我们继续以一个小的测试例子,来看一下,一个类数组,包含了哪些信息可供我们使用。

属性属性值
0[object Touch]
length1
itemfunction item(){[native code]}

上述列表中,是给出的当一个手指时的touches中,所包含的属性值,当然,从这里,也证明了TouchList对象时一个类数组。而item方法,也跟我们在NodeListHTMLCollection对象中的一样吧,获取第n个对象,只是我们大多数仍然会喜欢使用数组的方式进行操作吧。


那么我们继续看看,如果是多个点进行操作呢,如果是多个点的话,会不会有什么区别呢?首先,先把滑动区域进行限制,此时的滑动区域值限制在页面中有边框的区域。

这个时候,touches是没有什么问题的,有几根手指在操作,就会保存几个touch对象,而changedTouchestargetTouches却感觉是不灵敏的样子,尤其是changedTouches,因为当把touch事件的触发区域固定在一个小区域之后,changedTouches不论几根手指触发,都是只保存了一个touch对象,changedTouches这个类数组的长度一直只有一个元素。不理解(Android手机测试)。

如果不进行固定呢,把touchstart的监听绑定到document对象上面呢。

更让我疑惑的是,此时,这三个属性,都能正确的获取到触点的个数,有几根手指,就能有在各个属性中,就会保存多少个touch对象。

属性属性值
touches保存当前一个触摸个数的列表。只读属性(a list of Touches for every point of contact currently touching the surface.)
targetTouches保存当前一个触摸个数的列表。只读属性,该列表中包含的元素需要发生在当前事件所触发的元素之上(A list of Touches for every point of contact that is touching the surface and started on the element that is the target of the current event.)
changedTouches该属性是依据事件在触发的,并且获取到的一个列表。我的理解就是,只有事件变化时,才会更改该属性的取值。只读属性(For the touchstart event this must be a list of the touch points that just became active with the current event. For the touchmove event this must be a list of the touch points that have moved since the last event. For the touchend and touchcancel events this must be a list of the touch points that have just been removed from the surface. )

touches属性的解释,很明显,就是当每一个事件被触发时,就会获取此时每个触点的相关信息,并保存到touches对象中去。

targetTouches属性,却是要只有在被绑定的元素本身之上触发,才会被保存到targetTouches属性中去,因为之前的demo里,是把touchstart事件绑定到了document对象上,所以导致该属性获取的值域touches基本相同(按理说,这个属性获取到的touchlist列表,在一定的情况下,是和touches获取到的有区别的,但是我还是不能确定,这个区别到底是由什么因素引起,可以参考touch-14中的示例,但是并不能理解是什么原因导致的)。

changedTouches属性,按我的理解,就是在事件变化时,才会出现这个属性,对于这个确切的说法,还是先看下面的这个例子之后,才能更好的理解了。


这个时候,有一个有趣的问题就是,如果你以一个触点(一根手指)来操作的话,touchestargetTouches两个属性,当这个触点(这根手指)离开屏幕,触发touchend事件时,这两个属性中,是不包含任何对象的,就是说,这两个类数组的长度是0。而changedTouches却可以获取到这个触点的touch对象,为什么?

验证这个可以通过很简单的方法,用两个触点(两根手指),其中一个触点一直按着屏幕,而另外一个触点,触发touchend事件,可以看到这个时候,touchestargetTouches的属性中,数组长度为1,是那个一直接触者屏幕的触点的属性值。

从上面的两点,也可以理解到,这三个属性之间的差距,就拿touchend事件时的情况来说明吧(这个比较好理解),当touchend被触发时,手指时离开屏幕了,所以此时的touchestargetTouches属性的属性值,是不会再包含这个刚离开的触点的信息的,所以,当一个触点触发touchend事件时,touchestargetTouches属性值中的touch对象个数就是0了,表示当前一个触点也没有。

changedTouches的属性值,却是包含了触发touchend事件之前,所有的触点,比如一个触点时,这个时候,changedTouches的属性值中,就包含了一个touch对象的信息,代表刚触发touchend事件的这个触点的相关信息。

不知是否可以理解我之前的这些个说明。如有问题,请指出,非常感谢。

明白了touhestargetToucheschangedTouches三者的这一个小小的差别,也就能理解,为何在我们判断滑动方向时,使用changedTouches获取触点的在touchstarttouchend时的位置信息的原因了吧,关于这个,在下一篇文章中进行测试说明。

OK,暂时关于touch事件的属性方面,只想到了这么多,欢迎指教。

参考文章:http://www.zhangyunling.com/235.html(touch事件

简要的探讨一下移动端 touch 事件处理几个坑,以及相应的简单处理方法。

click 穿透

假设有个弹出层,上面有个关闭的按钮支持 touchend 触发后关闭,若正好下方有个元素支持 click 事件,在弹出层关闭后将会在下方元素触发 click 事件。这种效果肯定不是我们需要的,而且我们无法确定合适会在上方出现一个支持 touch 的弹出层,所以我认为最好的处理方式是禁用所有元素的 click 事件,相比 click 需要长达 1s 的触发时间,使用 touchend 可以获得更好的体验。

tap 事件的判定

一个正确的 tap 事件应当满足一下条件:

  1. 用户手指从屏幕移开时触发

  2. 不能在用户移动手指时触发(防止和滚动、拖拽事件的冲突)

  3. 多个手指同时触摸屏幕时不能触发

  4. 不应该触发 click 事件

具体实现代码可以参考 tap-event

使用原生的滚动事件

Android 4.0 以下是不支持原生的 webview 滚动的,所以只能使用 iscroll 之类的工具来模拟元素滚动。它的缺点就是有些过于的复杂,所以我还是会在条件允许的情况下使用原生的滚动。

启用原生滚动只需要给外层元素加上样式 -webkit-overflow-scrolling: touch; 即可,如果你的监听函数比较占用资源我们可以通过一个简单的 buffer 函数来限制它的触发间隔,例如:

function buffer(fn, ms) {
  var timeout;
  return function () {
    if (timeout) return;
    var args = arguments;
    timeout = setTimeout(function () {
      timeout = null;
      fn.apply(null, args);
    }, ms);
  }
}
document.querySelector('.scrollable').onscroll = buffer(onScroll, 100);

另外的建议就是不要在可滚动元素上使用阴影样式(text-shadow 和 box-shadow),因为它们非常影响性能,而且看上去也不怎么美观。

还有需要注意的是如果你需要启用apple-mobile-web-app-capable, 注意将apple-mobile-web-app-status-bar-style设置为black-translucent,否则会出现还差 22 像素滚动不到头的坑爹 bug。

禁用页面整体拖动

IOS下默认情况下用户的拖动操作在scroll滚到头以后会导致整体页面的滚动,一种方式是禁用掉 document 的 touchmove 原生触发

document.addEventListener('touchmove', function(e) {
  e.preventDefault();
});

此时原生的滚动是无法工作的,解决办法就是禁用滚动元素的 touchmove 事件冒泡

scrollable.addEventListener('touchmove', function (e) {
   e.stopPropagation();
});

另一种方式是判定滚动元素滚到头之后禁用掉默认的处理

var el = document.querySelector('.scrollable');var sy = 0;
events.bind(el, 'touchstart', function (e) {
  sy = e.pageY;
})

events.bind(el, 'touchmove', function (e) { 
 var down = (e.pageY - sy > 0);  //top
  if (down && el.scrollTop = el.scrollHeight - el.clientHeight) {
    e.preventDefault();
  }
})

我个人倾向于第二种方案,因为如果单纯的禁用 document 的 touchmove 监听,会导致一些处理的失效,比如说上面提到的 tap-event 模块。

拖动方向与距离

通过 event 的 pageX 和 pageY 属性即可计算,可参考 hammer.js


  在iPhone 3Gs发布的时候,其自带的移动Safari浏览器就提供了一些与触摸(touch)操作相关的新事件。随后,Android上的浏览器也实现了相同的事件。触摸事件(touch)会在用户手指放在屏幕上面的时候、在屏幕上滑动的时候或者是从屏幕上移开的时候出发。下面具体说明:

  touchstart事件:当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发。


  touchmove事件:当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。


  touchend事件:当手指从屏幕上离开的时候触发。


  touchcancel事件:当系统停止跟踪触摸的时候触发。关于这个事件的确切出发时间,文档中并没有具体说明,咱们只能去猜测了。


触摸事件还包含下面三个用于跟踪触摸的属性

       touches:表示当前跟踪的触摸操作的touch对象的数组。

  targetTouches:特定于事件目标的Touch对象的数组。

  changeTouches:表示自上次触摸以来发生了什么改变的Touch对象的数组。

每个Touch对象包含的属性如下。

  clientX:触摸目标在视口中的x坐标。

  clientY:触摸目标在视口中的y坐标。

  identifier:标识触摸的唯一ID。

  pageX:触摸目标在页面中的x坐标。

  pageY:触摸目标在页面中的y坐标。  

       screenX:触摸目标在屏幕中的x坐标。

screenY:触摸目标在屏幕中的y坐标。

  target:触目的DOM节点目标。

这里,除了前三种changedTouchestargetTouchestouches之外的其他属性,都是我们常见的一些属性值,所以这里对于这些属性就不做处理,而这三个新的属性,是只针对touch事件存在的属性值,并且是我们之后处理时,获取一些关键数据的属性,所以这里就只对这三个属性进行说明。

TouchList

看了上面的列表中的内容,首先先注意到的一点就是,TouchList对象,一个新的也是唯有touch事件的event对象中,才会出现的一种对象,在了解changedTouchestargetTouchestouches这三种属性之前,先对TouchList对象进行一个简单的了解。

TouchList是一个只读的类数组对象,它表示在当前的touch事件中,与触摸屏的接触点的个数,比如:如果你当前是三根手指在同时在触摸屏上,那么每一根手指都会有一个相对应的touch对象,来记录对应手指的操作相关的信息,这个时候的TouchList类数组中,就有三个数据,分别是这三根手指所对应的touch对象,而TouchList类数组的长度也就是3.

根据我们对类数组的了解,既然是类数组,那么就会有length属性,来表示这个类数组的长度,并且可以使用属性的中括号取值方式,取出对应位置的值。OK,下面我们继续以一个小的测试例子,来看一下,一个类数组,包含了哪些信息可供我们使用。

属性属性值
0[object Touch]
length1
itemfunction item(){[native code]}

上述列表中,是给出的当一个手指时的touches中,所包含的属性值,当然,从这里,也证明了TouchList对象时一个类数组。而item方法,也跟我们在NodeListHTMLCollection对象中的一样吧,获取第n个对象,只是我们大多数仍然会喜欢使用数组的方式进行操作吧。


那么我们继续看看,如果是多个点进行操作呢,如果是多个点的话,会不会有什么区别呢?首先,先把滑动区域进行限制,此时的滑动区域值限制在页面中有边框的区域。


这个时候,touches是没有什么问题的,有几根手指在操作,就会保存几个touch对象,而changedTouches和targetTouches却感觉是不灵敏的样子,尤其是changedTouches,因为当把touch事件的触发区域固定在一个小区域之后,changedTouches不论几根手指触发,都是只保存了一个touch对象,changedTouches这个类数组的长度一直只有一个元素。不理解(Android手机测试)。


如果不进行固定呢,把touchstart的监听绑定到document对象上面呢。


更让我疑惑的是,此时,这三个属性,都能正确的获取到触点的个数,有几根手指,就能有在各个属性中,就会保存多少个touch对象。

属性属性值
touches保存当前一个触摸个数的列表。只读属性(a list of Touches for every point of contact currently touching the surface.)
targetTouches保存当前一个触摸个数的列表。只读属性,该列表中包含的元素需要发生在当前事件所触发的元素之上(A list of Touches for every point of contact that is touching the surface and started on the element that is the target of the current event.)
changedTouches该属性是依据事件在触发的,并且获取到的一个列表。我的理解就是,只有事件变化时,才会更改该属性的取值。只读属性(For the touchstart event this must be a list of the touch points that just became active with the current event. For the touchmove event this must be a list of the touch points that have moved since the last event. For the touchend and touchcancel events this must be a list of the touch points that have just been removed from the surface. )

touches属性的解释,很明显,就是当每一个事件被触发时,就会获取此时每个触点的相关信息,并保存到touches对象中去。


targetTouches属性,却是要只有在被绑定的元素本身之上触发,才会被保存到targetTouches属性中去,因为之前的demo里,是把touchstart事件绑定到了document对象上,所以导致该属性获取的值域touches基本相同(按理说,这个属性获取到的touch的list列表,在一定的情况下,是和touches获取到的有区别的,但是我还是不能确定,这个区别到底是由什么因素引起,可以参考touch-14中的示例,但是并不能理解是什么原因导致的)。


changedTouches属性,按我的理解,就是在事件变化时,才会出现这个属性,对于这个确切的说法,还是先看下面的这个例子之后,才能更好的理解了。


这个时候,有一个有趣的问题就是,如果你以一个触点(一根手指)来操作的话,touches和targetTouches两个属性,当这个触点(这根手指)离开屏幕,触发touchend事件时,这两个属性中,是不包含任何对象的,就是说,这两个类数组的长度是0。而changedTouches却可以获取到这个触点的touch对象,为什么?

验证这个可以通过很简单的方法,用两个触点(两根手指),其中一个触点一直按着屏幕,而另外一个触点,触发touchend事件,可以看到这个时候,touches和targetTouches的属性中,数组长度为1,是那个一直接触者屏幕的触点的属性值。

从上面的两点,也可以理解到,这三个属性之间的差距,就拿touchend事件时的情况来说明吧(这个比较好理解),当touchend被触发时,手指时离开屏幕了,所以此时的touches和targetTouches属性的属性值,是不会再包含这个刚离开的触点的信息的,所以,当一个触点触发touchend事件时,touches和targetTouches属性值中的touch对象个数就是0了,表示当前一个触点也没有。


而changedTouches的属性值,却是包含了触发touchend事件之前,所有的触点,比如一个触点时,这个时候,changedTouches的属性值中,就包含了一个touch对象的信息,代表刚触发touchend事件的这个触点的相关信息。


不知是否可以理解我之前的这些个说明。如有问题,请指出,非常感谢。


明白了touhes,targetTouches和changedTouches三者的这一个小小的差别,也就能理解,为何在我们判断滑动方向时,使用changedTouches获取触点的在touchstart和touchend时的位置信息的原因了吧,关于这个,在下一篇文章中进行测试说明。


OK,暂时关于touch事件的属性方面,只想到了这么多,欢迎指教。

参考文章:http://www.zhangyunling.com/235.html(touch事件


简要的探讨一下移动端 touch 事件处理几个坑,以及相应的简单处理方法。

click 穿透

假设有个弹出层,上面有个关闭的按钮支持 touchend 触发后关闭,若正好下方有个元素支持 click 事件,在弹出层关闭后将会在下方元素触发 click 事件。这种效果肯定不是我们需要的,而且我们无法确定合适会在上方出现一个支持 touch 的弹出层,所以我认为最好的处理方式是禁用所有元素的 click 事件,相比 click 需要长达 1s 的触发时间,使用 touchend 可以获得更好的体验。

tap 事件的判定

一个正确的 tap 事件应当满足一下条件:

  1. 用户手指从屏幕移开时触发

  2. 不能在用户移动手指时触发(防止和滚动、拖拽事件的冲突)

  3. 多个手指同时触摸屏幕时不能触发

  4. 不应该触发 click 事件

具体实现代码可以参考 tap-event

使用原生的滚动事件

Android 4.0 以下是不支持原生的 webview 滚动的,所以只能使用 iscroll 之类的工具来模拟元素滚动。它的缺点就是有些过于的复杂,所以我还是会在条件允许的情况下使用原生的滚动。

启用原生滚动只需要给外层元素加上样式 -webkit-overflow-scrolling: touch; 即可,如果你的监听函数比较占用资源我们可以通过一个简单的 buffer 函数来限制它的触发间隔,例如:

function buffer(fn, ms) {
  var timeout;
  return function () {
    if (timeout) return;
    var args = arguments;
    timeout = setTimeout(function () {
      timeout = null;
      fn.apply(null, args);
    }, ms);
  }
}
document.querySelector('.scrollable').onscroll = buffer(onScroll, 100);

另外的建议就是不要在可滚动元素上使用阴影样式(text-shadow 和 box-shadow),因为它们非常影响性能,而且看上去也不怎么美观。

还有需要注意的是如果你需要启用apple-mobile-web-app-capable, 注意将apple-mobile-web-app-status-bar-style设置为black-translucent,否则会出现还差 22 像素滚动不到头的坑爹 bug。

禁用页面整体拖动

IOS下默认情况下用户的拖动操作在scroll滚到头以后会导致整体页面的滚动,一种方式是禁用掉 document 的 touchmove 原生触发

document.addEventListener('touchmove', function(e) {
  e.preventDefault();
});

此时原生的滚动是无法工作的,解决办法就是禁用滚动元素的 touchmove 事件冒泡

scrollable.addEventListener('touchmove', function (e) {
   e.stopPropagation();
});

另一种方式是判定滚动元素滚到头之后禁用掉默认的处理

var el = document.querySelector('.scrollable');var sy = 0;
events.bind(el, 'touchstart', function (e) {
  sy = e.pageY;
})

events.bind(el, 'touchmove', function (e) { 
 var down = (e.pageY - sy > 0);  //top
  if (down && el.scrollTop = el.scrollHeight - el.clientHeight) {
    e.preventDefault();
  }
})

我个人倾向于第二种方案,因为如果单纯的禁用 document 的 touchmove 监听,会导致一些处理的失效,比如说上面提到的 tap-event 模块。

拖动方向与距离

通过 event 的 pageX 和 pageY 属性即可计算,可参考 hammer.js


转载本站文章《移动端的touch事件处理》, 请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/html5/2017_0216_7950.html