简单聊聊touch事件

这几天在整理之前的内容,把散落在各地的东西都整理在一起,一篇14年的文章,收录到这里;主要是对移动端touch事件的一些说明

引子

众前端同学应该都处理过PC时代的鼠标click事件,随着时间的发展,我们的领域从PC蔓延到了移动端,从最熟悉的click变化成了触摸touch。鼠标 click->手指、触摸笔等等 touch

开启触摸“时代”

触摸事件

接口

TouchEvent代表当触摸行为在平面上变化的时候发生的事件

Touch 代表用户与触摸平面间的一个触摸点

TouchList 代表一系列的Touch;一般用户多个触摸点触碰平面的时候

DocumentTouch 包含了一些创建Touch和TouchList对象的方法 ——MDN: Touch Events

TouchEvent接口

  • 类型: touchstart、touchmove、touchend、touchenter、touchleave、touchcancel

  • 属性:altkey、changedTouchs、ctrlKey、metaKey、shiftKey、targetTouches、touches、type、target

    ——例子

常用的touch事件类型

  • touchstart : 手指放在一个dom元素上
  • touchmove: 手指拖动一个dom元素
  • touchend: 手指从一个dom元素上移开
  • touchenter : 移动的手指进入一个dom元素
  • touchleave : 移动的手指离开一个dom元素
  • touchcancel : 触摸中断

TouchEvent事件注意点

可以通过检查触摸事件的TouchEvent.type属性来确定当前事件属于哪种类型

注意:在很多情况才,触摸事件和鼠标事件会同时被触发(目的是让没有对触摸设备优化的代码仍然可以在触摸设备上正常工作)。如果你使用了触摸事件,可以调用event.preventDefault()来阻止鼠标事件被触发

看看这些常用的手势

各种手势

推荐一个js手势库:Hammer.js

zepto touch

300ms延迟

在手机上,touchstart的触发时间与最后click的触发时间的时间延迟

原因

追溯到07年Apple准备发布 iPhone的时候,当时为了解决手机上web页面太小的问题,所以有了“双击屏幕放大”的功能(double tap to zoom)

“双击屏幕放大”的功能导致了300MS的click事件的延迟,因为当用户触摸屏幕的时候,浏览器不知道用户是要double tap吗?还是要click?所以浏览器在第一次tap事件之后会等300MS来判断到底是double tap 还是click

解决方法

-浏览器层面:在meta里面,加入信息,阻止双击放大的功能

1
<meta name="viewport" content="width-device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />

-代码层面:fastclick.js

-代码:利用touchstart、touchmove等内置事件的封装,来实现手机上的各种手势,比如tap、swipe等

zepto-touch如何解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$(documnet)
.on('touchstart ...',function(e){
...
now = Date.now()
delta = now - (touch.last || now)
if(delta > 0 && delta <=250 )touch.isDoubleTap = true
touch.last = now
})
.on('touchmove ...',function(e){
})
.on('touchend ...',function(e){
...
if(deltaX < 30 && deltaY < 30){
var event = $.Event('tap')
touch.el.trigger(event)
}
});

一句话总结:touch模块绑定事件touchstart、touchmove、touchend到document上,然后通过计算事件触发的时间差、位置差来实现自定义的tap、swipe等

Ghost Click

  • 当手指触摸到屏幕上的时候触发touchstart事件

  • 当手指离开屏幕的时候触发touchend事件

  • 浏览器等待300ms看是否有另外一个tap事件

  • 如果没有另外的这个tap事件,那么click事件就会被触发,这就是ghost click

    zepto touch的一个小问题

    没有阻止ghost click的发生 例子

    如何阻止Ghost click

  • 在touchstart的时候调用preventDefault(),在大多数的浏览器上能解决这个问题,但是在这个dom元素上就不能触发scrolling滚动

  • 在touchend的时候调用preventDefault(),不过只有一小部分的浏览器支持

  • 如果页面是不可scalable的时候,一些浏览器不会等待300ms去触发click,click会在touchend的时候很快的触发,这个时候可以用click来代替tap

——小测试

以上的三种方法基本就没有解决问题

看看一个关于上面各种方法解决问题的测试图

原因

  • touchstart、touchmove、touchend事件绑定到document上
  • 事件冒泡
  • 事件触发时位于当前页面最前面的元素

用preventDefault()只是阻止默认行为,不会阻止事件的冒泡的行为,最后事件还是会冒泡到document上,此时,如果绑定了click事件的dom元素位于页面的最前面,就会触发click事件的发生

回顾下事件的捕获和冒泡行为

事件行为

问题假设

不要把事件绑定到document上,然后利用preventDefault(), 是否能解决此问题?

重写touch模块

源码

理念

  • 过touchstart,touchmove,touchend这些内置的事件来实现tap,swipe等手势
  • 计算touch事件触发的时间差,位置差来实现了自定义的tap,swipe等事件
  • tap,swipe这些事件通过自定义事件来触发

自定义事件

内置的事件会由浏览器根据某些操作进行触发,

自定义的事件就需要人工触发

1
2
3
4
5
6
//创建haosou事件
var event = new Event('haosou');
//监听haosou事件
elem.addEventListener('haosou', function(e){...},false);
//分发haosou事件
elem.dispatchEvent(event);

——例子

主代码片段1

1
2
3
4
5
 //为特定的元素绑定touch事件
el.addEventListener('touchstart', func, false);
el.addEventListener('touchmove', func, false);
el.addEventListener('touchend', func, false);
el.addEventListener('touchcancel', func, false);

主代码片段2

1
2
3
4
5
//在touchend的时候实现自定义的事件
var evt = createCustomEvent('tap',e);
if (evt && !e.target.dispatchEvent(evt) ){
e.preventDefault();
}

主代码片段3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//自定义事件
function createCustomEvent(touchName,detail){
var evt;
var params = {
bubbles:true,
cancelable: true,
detail: detail ? detail : undefined
};

if (window.CustomEvent) {
evt = new window.CustomEvent(touchName, {
bubbles: params.bubbles,
cancelable: params.cancelable,
detail : params.detail
});
} else {

try {
evt = document.createEvent("CustomEvent");
evt.initCustomEvent(touchName, params.bubbles, params.cancelable, params.detail);
} catch (error) {
//某些老版本的2.3手机不支持CustomEvent,比如索爱2.3系统
evt = document.createEvent("Event");
for (var param in params) {
evt[param] = params[param];
}
evt.initEvent(touchName, params.bubbles, params.cancelable);
}
}

return evt;
}

假设成立吗?

如果在touchstart上用preventDefault(),没办法使用事件代理

强行阻止掉click

1
2
3
4
5
6
7
8
9
10
11
12
//阻止点click事件的发生,
//保证在同一个位置的tap事件以后,不会再有click事件
window.addEventListener('click', function (e) {
var time_threshold = 500,
space_threshold = 100;
if (new Date().valueOf() - this.endTime <= time_threshold
&& Math.abs(e.clientX-this.endX)<=space_threshold
&& Math.abs(e.clientY-this.endY)<=space_threshold) {
e.stopPropagation();
e.preventDefault();
}
}, true);

在业务中发展

  • 需要触发子元素的click事件
  • swipe不能满足,需要在滑动过程监听事件

最后发展出的独立touch模块事件:

tap //轻点触摸支持click

tapnoclick //轻点触摸去掉click

swipeleft // 左滑,在手指离开屏幕以后触发

swiperight // 右滑,在手指离开屏幕以后触发

panleft // 左滑,在手指在屏幕上滑动时触发

panright // 右滑,在手指在屏幕上滑动式触发