移动端交互手势详解及实现
一丶概述
如今移动端设备大行其道,前端也走进了移动的领域。在写移动端页面的交互效果的时候,我么难免要接触一些复杂的手势,而不仅仅像pc端那样简单的鼠标事件。手势实际上是一种输入模式。我们现在在直观意义上理解的人机交互是指人与机器之间的互动方式,这种互动方式经历了鼠标、物理硬件、屏幕触控、远距离的体感操作的逐步发展的过程。
二丶移动端手势事件
在浏览器中,为我们提供的手势并不算多,主要有:
- touchstart 当手指触摸屏幕时触发
- touchmove 当手指在屏幕滑动时不断的触发
- touchend 当手指从屏幕上移开时触发
- touchcancel 当系统停止跟踪触摸时触发
是不是感觉很少,safari还为我们提供了三个独有的手势事件(用于复杂的手势),然而也仅仅只能在safari中使用
- gesturestart 当一个手指已经按在屏幕上,另一个手指也按上时触发
- gesturechange 当触摸屏幕上任何一个手指发生变化时触发
- gestureend 当任何一个手指从屏幕上移开时触发
最后呢,让我们看看移动设备上究竟有哪儿手势需要我们使用
三丶让JS支持这些手势
目前看来,我们能用的也就只有touchstart,touchmove, touchend, touchcancel这四个手势,那么如何才能利用这四个手势支持众多的交互效果呢?首先我们从最简单的手势开始。简单的手势也就是说是单点触控,我们主要来实现如下几个手势:
- tap 轻触
- doubletap 连续两次轻触
- press 长按
- pan 平移
- flick 轻拂
首先我们要解决如何触发自定义事件(已经了解自定事件的可以跳过):
//自定义一个事件
document.body.addEventListener("tap", function(event) {
console.log("tap事件触发")
}, false)
//触发自定义事件
function fireEvent(element, type, extra) {
var event = doc.createEvent('HTMLEvents');
event.initEvent(type, true, true);
if (typeof extra === 'object') {
Util.extends(event, extra); //浅拷贝
}
element.dispatchEvent(event);
}
fireEvent(document.body, "tap", {}); //触发tap事件
我们在整个事件模拟中定义一个中间状态 evet.status 来表示当前的触摸状态,接下来我们就利用touchstart,touchmove,touchend来可以实现自己的触摸事件了
tap事件:当touchstart触发时,我们将event.status状态改为 tapping。在touchend触发时,如果event.status依然为tapping则,触发tap事件。
doubletap事件:在触发tap事件的时候,我们用一个变量lastTime记录当前时间。下一次触发tap时,用当前时间和lastTime做对比,如果小于300ms则触发doubletap事件
pess事件:当touchstart触发时,我们定义一个setTimeout的函数(500ms),如果500ms后仍然没有touchend触发,则定时函数将event.staus状态改为pressing。当touchend触发时,检测到状态为pressing则触发press事件。
pan事件:我们在touchmove中检测当前状态是tapping和pressing时,并且手指移动距离大于10px则,触发pan平移事件。这个移动距离用event.touches[0].clientX - lastTouch.clientY 来检测就好(利用lastTouch记录,起始手指的event对象)。
flick事件:这个事件就是"刷~刷"的划过屏幕的交互效果,在touchend时通过pan事件的移动距离和移动事件算出速度(注意是X和Y轴的合速度),如果速度大于0.5,并且整个触摸过程时间小于100ms,则触发flick事件。
是不是很简单的用最原始的浏览器事件就能实现这些内容。
接下来让我们看看两个手指的事件如果实现。
四丶实现多指触控
在实现多指触控的时候,我们需要了解一下触摸过程中event用来保存多个手指信息的三个属性:
- touches当前屏幕上所有触摸点的集合列表
- targetTouches绑定事件的那个结点上的触摸点的集合列表
- changedTouches触发事件时改变的触摸点的集合
这三个有什么区别?举例来说,比如div1, div2只有div2绑定了touchstart事件,第一次放下一个手指在div2上,触发了touchstart事件,这个时候,三个集合的内容是一样的,都包含这个手指的touch,然后,再放下两个手指一个在div1上,一个在div2上,这个时候又会触发事件,但changedTouches里面只包含第二个第三个手指的信息,因为第一个没有发生变化,而targetTouches包含的是在第一个手指和第三个在div2上的手指集合,touches包含屏幕上所有手指的信息,也就是三个手指。这样是不是就很很清楚了。下面我们要根据上面的内容,继续解决一个问题:当两个手指作用在不同的节点上应该触发哪个节点的事件呢?
这里我们规定,如果触发在了两个不同节点上,我们去两个节点公有的最近父节点,作为触发的目标。寻找共有最小父节点代码如下:
//判断节点ele1是否包含ele2
function contains(ele1, ele2) {
return ele1.contains ? ele1 != ele2 && ele1.contains(ele2) : !!(ele1.compareDocumentPosition(ele2) & 16);
}
//获得共有最近的父节点
function getCommonRootNode(ele1, ele2) {
while (ele1) {
if (contains(ele1, ele2) || ele1 === ele2) {
return ele1;
}
ele1 = ele1.parentNode;
}
return null;
}
这样我们解决了,如何找到多个手指信息和触发哪个节点的问题。最后一个问题,当给了我们这些信息我们怎么能用?比如计算旋转手势,缩放手势啊什么的。
这里我们仅考虑两个手指的多点触控。我们设touchstart阶段的两个手指坐标为 A(x1, y1) B(x2, y2)。touchmove过程中的两个手指的坐标为 C(x3, y3) D(x4, y4)。
rotate旋转:计算AB,CD线段与坐标轴的夹角,对角度相减即得到旋转角度。
scale 缩放:计算AB线段长度和CD线段长度(勾股定理),两条线段做比值就好。
translate平移: 平移的话我们只计算A点到C点的x坐标变化量。
具体代码如下:
function calcAction(x1, y1, x2, y2, x3, y3, x4, y4) {
let rotate = Math.atan2(y4 - y3, x4 - x3) - Math.atan2(y2 - y1, x2 - x1),
scale = Math.sqrt((Math.pow(y4 - y3, 2) + Math.pow(x4 - x3, 2)) / (Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2))),
translate = [x3 - scale * x1 * Math.cos(rotate) + scale * y1 * Math.sin(rotate), y3 - scale * y1 * Math.cos(rotate) - scale * x1 * Math.sin(rotate)];
return {
rotate: rotate,
scale: scale,
translate: translate,
/**
* |ax + cy + e|
* |bx + dy + f|
* | 0 + 0 + 1|
*/
martrix: [
[scale * Math.cos(rotate), -scale * Math.sin(rotate), translate[0]],
[scale * Math.sin(rotate), scale * Math.cos(rotate), translate[1]],
[0, 0, 1]
]
}
}
了解这些内容,你就可以在touchmove过程中完成对两个手指的旋转缩放平移等交互效果进行封装了。是不是很简单呢!
当然完整的事件过程要分start,move,end这三种情况,在实现的时候要分别给予对应的实现就可以了。都逃不开对touchstart,touchmove,touchend的利用。
五丶实现案例
基于上面的方案,我实现了一个对移动端手势的封装库,包含以上所有的手势。犹豫这里代码运行不能模拟手机环境,我就不贴代码了。
感兴趣的同学可以访问:https://github.com/T-phantom/si-gesture 上面有具体的使用方法和带有详细注释的源码哦,欢迎start。