移动端 Touch 事件介绍

本文主要介绍 TouchEvent 相关的一些对象与属性如 Touch, TouchList, touhces, targetTouches 等,以及使用的注意点和误区。

触摸事件有以下几种类型:touchstart,touchmove,touchend这三种用的比较多,还有不常用的touchcancel事件。当然 MDN上还介绍了touchenter,touchleave事件,具体适用的场景及兼容性如何还未做测试,感兴趣的可自行研究。

js中不同的事件类型,event对象包含的属性也有所差异。我们先了解几个TouchEvent涉及的对象。

提示:文中的demo都是在 chrome 模拟器,iPhone6s(iOS9.3.2) safari,iOS微信上运行,安卓的兼容性未做测试

Touch

Touch对象代表一个触点,可以通过event.touches[0]获取,每个触点包含位置,大小,形状,压力大小,和目标 element属性。

{
    screenX: 511, 
    screenY: 400,//触点相对于屏幕左边沿的Y坐标
    clientX: 244.37899780273438, 
    clientY: 189.3820037841797,//相对于可视区域
    pageX: 244.37, 
    pageY: 189.37,//相对于HTML文档顶部,当页面有滚动的时候与clientX=Y 不等
    force: 1,//压力大小,是从0.0(没有压力)到1.0(最大压力)的浮点数
    identifier: 1036403715,//一次触摸动作的唯一标识符
    radiusX: 37.565673828125, //能够包围用户和触摸平面的接触面的最小椭圆的水平轴(X轴)半径
    radiusY: 37.565673828125,
    rotationAngle: 0,//它是这样一个角度值:由radiusX 和 radiusY 描述的正方向的椭圆,需要通过顺时针旋转这个角度值,才能最精确地覆盖住用户和触摸平面的接触面
    target: {} // 此次触摸事件的目标element
}

identifier
这个属性大家可能有疑惑,使用 Chrome 的模拟器发现多次触摸动作,值始终不变。用真机测试则不会有问题(我这里用的safari连接mac调试)。每次触摸包括start,move,end这整个过程,标志符都不变。下一次触摸动作开始,标志符就会变化。

screenY clientY
在 safari 中 screenYclientY值是相等的,在iOS微信中两个数值不等,但单位应该也不一样。

radiusX radiusY rotationAngle
测试过程中safari及微信内置浏览器都不支持这些属性,chrome模拟器可以。

TouchList

Touch对象构成的数组,通过event.touches取到。一个Touch对象代表一个触点,当有多个手指触摸屏幕时,TouchList就会存储多个Touch对象,前面说到的identifier就用来区分每个手指对应的Touch对象。

TouchEvent

TouchEvent就是用来描述手指触摸屏幕的状态变化事件,除了一般DOM事件中event对像具备的属性,还有一些特有的属性。

touches

一个TouchList对象,包含当前所有接触屏幕的触点的Touch对象,不论 touchstart 事件从哪个elment上触发。

targetTouches

也是一个TouchList对象,包含了如下触点的 Touch 对象:touchstart从当前事件的目标element上触发

这里大家可能产生了疑惑,这两个对象到底有什么区别?尤其是我们使用chrome模拟器中运行 demo,打印两个对象发现他们其实是一样的。
这两个对象的区别可以类比event.targetevent.currentTarget 的区别,如果以前没留意,自行 js 高级程序设计。

我们先看一个 demo2,来了解 touch 事件的特性。
在线编辑: http://jsrun.net/3XKKp
预览地址: http://jsrun.net/rtd/3XKKp

大家进行以下两个操作,观察控制台发现了什么?
操作一:一根手指触摸蓝色box,并滑动,继续滑动出蓝色box
操作二:一根手指触摸非蓝色box区域,然后慢慢滑动到蓝色box

大家会发现:操作一中即使滑出蓝色box,而touchmovetouchend事件会继续触发,touches,targetTouches存储着相同的 Touch 对象,touchmove事件的目标元素仍然是box。
操作二中相关的 touch 事件都不会触发。很神奇的是 touchmove 事件,明明在 box 上滑动,却不会触发 touchmove 事件。

我们可以猜测,touch相关的事件是一个整体,一开始touchstart不可能被触发,则后续touch事件也不会被触发。当然你可以不监听 touchstart 事件,按照操作一 touchmove,touchend 还是可以触发的。

再看下面这个demo2
在线编辑:http://jsrun.net/XXKKp
访问地址:http://jsrun.net/rtd/XXKKp

这里我们对白色区域body也添加了 touch 事件的监听,继续上述 demo1中的两个操作。
我们可以发现:

操作一可以发现:touch 相关的事件可以冒泡,触发了 box,body的touch事件。操作二只能触发 body 的touch 事件,和demo1同理。

我们可以观察下操作一的两个对象TouchEvent.targetTouches,TouchEvent.touches,无论是box还是body触发的 touch 事件,他们的存储的 Touch对象都是相同的,而且 target 都是 box。
接下来进行操作三:

用两根手指,一根手指触摸蓝色box,另一根触摸白色区域,然后滑动。

然后再次比较下targetTouchestouches,就可以发现他们的不同。

changedTouches

也是一个 TouchList 对象,对于 touchstart 事件, 这个 TouchList 对象列出在此次事件中新增加的触点。对于 touchmove 事件,列出和上一次事件相比较,发生了变化的触点。对于 touchend ,列出离开触摸平面的触点(这些触点对应已经不接触触摸平面的手指)。

touchend这里要特别注意,touches和targetTouches只存储接触屏幕的触点,要获取触点最后离开的状态要使用changedTouches。

之前也经常用touches[0]来获取Touch 对象,如果知道了 touches,targetTouches,changedTouches 的不同之处。在编写代码时可以更好的选择使用,程序也可以更严谨。

touch事件封装

(function () {
    var coord={},
        start={},
        el;

    document.addEventListener('touchstart', touchStart);
    document.addEventListener('touchmove',touchMove);
    document.addEventListener('touchend',touchEnd);
    document.addEventListener('touchcanel',touchCancel);

    function newEvent(type){
        return new Event(type,{ bubbles: true,cancelable: true});
    }

    function touchCancel () {
        coord = {}
    }

    function touchStart(e){
        var c = e.touches[0];
        start = {
            x: c.clientX,
            y: c.clientY,
            time: Date.now()
        };
        el= e.target;
        el='tagName' in el ? el : el.parentNode;
    }

    function touchMove(e){
        var t = e.touches[0];
        coord = {
            x: t.clientX - start.x,
            y: t.clientY - start.y
        }
    }

    function touchEnd(){
        var touchTimes = Date.now() - start.time,
                c = 250 > touchTimes && Math.abs(coord.x) > 20 || Math.abs(coord.x) > 80,
                s = 250 > touchTimes && Math.abs(coord.y) > 20 || Math.abs(coord.y) > 80,
                left = coord.x < 0,
                top = coord.y < 0;
        if (250 > touchTimes && (isNaN(coord.y) || Math.abs(coord.y)) < 12 && (isNaN(coord.x) || Math.abs(coord.x) < 12)) {
            el.dispatchEvent(newEvent('tap'));
        }else if(750<touchTimes && (isNaN(coord.y) || Math.abs(coord.y)) < 12 && (isNaN(coord.x) || Math.abs(coord.x) < 12)){
            el.dispatchEvent(newEvent('longTap'));
        }
        c ? el.dispatchEvent(left ? newEvent('swipeLeft') : newEvent('swipeRight')) : s && el.dispatchEvent(top ? newEvent('swipeUp') : newEvent('swipeDown'));

        coord={};
    }
}());

移动端 Touch 事件的使用与思考(1)

touch.js()原生JS,支持tap,longTap,swipeLeft,swipeRight,SwipeTop,swipeDown事件。

移动项目开发过程中,经常需要用到滑动的事件来处理一些效果。通常情况下,我们会通过  touchstart->touchmove->touchend  的过程来定义这个事件。这些事件的触发顺序是  touchstart, touchmove, touchmove ….. touchend  。绝大部分平板或手机也正如我们想象的那样有序执行着。但是以Android 4.0.4为首的一些可恶分子却有些不听话:他们的touchend事件没有如预期的那样触发。

监听这些事件我们会发现,当只是轻点一下屏幕时,touchend可以正常触发。但是只要当 touchmove 被触发之后,touchend 就不会再被触发了,而且 touchmove 也没有持续触发。

在网上搜集了一些资料显示,这是 Android 上浏览器的bug
> On Android ICS if no preventDefault is called on touchstart or the firsttouchmove,
> further touchmove events and the touchend will not be fired.

正如提到的我们只需要在 touchstart 或者 touchmove 里执行一下 e.preventDefault(); 就可以避免这个bug。但是,问题来了:添加了 preventDefault 之后,正常的scroll事件也被屏蔽了!我们意外的发现滚动条也不能用了!

于是,我们开始尝试各种添加preventDefault事件的时机:闭包,延迟,判断等一一用上。最终焦点落在了firsttouchmove上,于是有了以下代码。

var touchY = 0;
$(this).on('touchstart', function(e){
    var touch = e.touches[0];
    touchY = touch.clientY;
}).on('touchmove', function(e){
    var touch = e.touches[0]
    if(Math.abs(touch.clientY - touchY) < 10){
        e.preventDefault();
    }
}).on('touchend', function(){
    // 你的滑动事件
});

基本上主要的思想就是在 touchmove 的时候判断出纵轴的位移量,当小于某个值的时候就认为是在执行左右滑动,且需要执行 preventDefault 来确保 touchend 可以正常触发。当然,如果你有横向滚动条且想绑定上下滑动事件的话就需要自己修改一下代码。

touchEnd事件在Android的某些机子上兼容性不是很好,有些无法触发的bug,看一下这篇文章是怎么解决:彻底解决低端安卓手机touchend事件不触发(考虑scroll)

本次移动端开发时遇见了安卓4.2系统不能触发touchend的问题,有以下需求。

1. 横滑轮播图

2.下拉刷新页面内容

3.body滚动条不能失效

开始在轮播图touchmove事件中阻止了浏览器默认行为,此时touchend事件可以触发。

//拖拽轮播图
   parentNode.addEventListener('touchmove',function(e) {     
                    e.preventDefault();     
            }) 

然后复制了一份在下拉刷新事件中(此时下拉刷新也OK了)

//下拉刷新代码  
document.addEventListener('touchmove', function(e) {
        if (getTopDistance() <= 10) {
                e.preventDefault();
        }
    });

不过此时新的问题又出来了,页面竟然不能上下滚动了,经过分析得出结论在document的touchmove事件中阻止了浏览器默认行为导致页面不能上下滑动。

最终参考了老外的一篇文章解决此问题。(横滑炒过7认为是拖拽录播图)

     parentNode.addEventListener('touchmove',function(e) {
                var _x = e.touches[0].pageX;
                if((Math.abs(_x-parentNode.startX)>7)){
                    e.preventDefault();
                }
                e.stopPropagation();
            }) 

下拉刷新时也加上判断条件决定是否阻止浏览器默认行为(竖直滚动超过10阻止浏览器默认行为)

document.addEventListener('touchmove', function(e) {
      
        if (getTopDistance() <= 10) {//当滚动条位置小于10
            // alert('<');
            var _x = e.touches[0].pageX;
            var _y = e.touches[0].pageY;

            if (_y - obj.y > 10) {//滚动距离大于10
                e.preventDefault();
     
            }

        }
    });

/*获得滚动条位置
*/
function getTopDistance() {
return document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
}


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流