简介在这个触屏时代,人性化的手势操作已经渗透到我们生活的每一个角落。现代应用越来越注重与用户的交互和体验。手势是最直接有效的交互方式。一个好的手势交互可以降低用户的使用成本和流程,大大提升用户体验。最近公司的很多项目对手势的需求比较大,现有的手势库无法完全覆盖,所以开发了一个轻量级易用的移动端手势库。本篇博文主要从前端的角度分析了移动端常用手势的原理和学习过程中用到的数学知识。希望能给大家一点点启发,也期待大神们指出不足甚至错误,谢谢。主要讲解一下项目中经常用到的五个手势:拖动:拖拽双指缩放:捏合双指旋转:旋转单指缩放:singlePinch单指旋转:singleRotateTips:因为tap和swipe在很多基础库中都有,为方便起见,故不包括在内,但如有需要可以扩展;实现原理众所周知,所有手势都是基于浏览器原生事件touchstart、touchmove、touchend、touchcancel的上层封装,所以封装的思路是通过各个独立的事件回调仓库的handleBus,然后在原生触摸事件中,在符合条件的时机触发并发送计算出的参数值,完成手势操作。实现原理比较简单明了,不用着急,我们先梳理一下用到的一些数学概念,结合代码,把数学应用到实际问题中。数学部分可能比较枯燥,但希望大家继续阅读,相信会受益匪浅。基础数学知识函数我们常用的坐标系属于线性空间,或者说向量空间(VectorSpace)。这个空间是由点(Point)和向量(Vector)组成的集合;点(Point)可以理解为我们的坐标点,比如原点O(0,0),A(-1,2),通过nativeevents对象的触摸可以得到触摸点的坐标,而参数索引代表接触点;向量(Vector)是坐标系中既有大小又有方向的线段,例如从原点O(0,0)指向点A(1,1)称为向量a,则a=(1-0,1-0)=(1,1);如下图所示,其中i和j向量称为坐标系的单位向量,也称为基向量,我们常用的坐标系单位为1,即i=(1,0);j=(0,1);获取向量的函数:向量模表示向量的长度,记为|a|,是标量,只有大小,没有方向;几何意义表示以x、y为直角边的直??角三角形的斜边,由勾股定理计算;getLength函数:向量的量积向量也有可操作的属性,可以进行加减乘除、量积、向量积等操作。接下来介绍我们使用的量积的概念,也称为点积,其定义为公式:当a=(x1,y1),b=(x2,y2)时,则a·b=|a|·|b|·cosθ=x1·x2+y1·y2;共线定理共线,即两个向量处于平行状态,当a=(x1,y1),b=(x2,y2)时,则存在唯一实数λ,使得a=λb,后代入坐标点,可得x1·y2=y1·x2;所以当x1·y2-x2·y1>0时,斜率ka>kb,所以此时b向量相对于a向量为顺时针方向,反之为逆时针方向;旋转角度可以通过量积公式求出两个向量夹角:cosθ=(x1x2+y1y2)/(|a||b|);那么我们就可以通过共线定理来判断旋转的方向。函数定义为:矩阵和变换由于空间最本质的特点是可以容纳运动,所以在线性空间中,我们用向量来描述物体,用矩阵来描述物体的运动;矩阵如何描述运动?我们知道,通过一个坐标系基向量可以确定一个向量,比如a=(-1,2),我们通常约定的基向量是i=(1,0)j=(0,1);因此:a=-1i+2j=-1(1,0)+2(0,1)=(-1+0,0+2)=(-1,2);矩阵变换实际上是变换基vector通过矩阵,从而完成vector的变换;比如上面的栗子通过矩阵(1,2,3,0)对a向量进行变换,此时基向量i从(1,0)变换到(1,-2),j从(0,1)到(3,0),按照上面的推导,则a=-1i+2j=-1(-1,2)+2(3,0)=(5,-2);如下图所示:图A表示变换前的坐标系,此时a=(-1,2),经过矩阵变换后,基向量i,j的变换引起坐标系的变换,即变成下图中的B,所以a向量由(-1,2)变换为(5,-2);实际上,向量与坐标系的关系保持不变(a=-1i+2j),引起坐标系变化的是基向量,然后坐标系继续使用关联导致向量的变化;结合代码,其实csstransform等变换都是通过matrix进行的,和我们平时写的translate/rotate语法类似是一种封装的语法糖,使用起来方便快捷,以后会在底层转化为矩阵形式。比如transform:translate(-30px,-30px)编译后会转化为transform:matrix(1,0,0,1,30,30);通常在二维坐标系中,只需要一个2X2的矩阵就足以描述所有的变换,但是由于CSS处于3D环境,所以在CSS中使用了一个3X3的矩阵,表示为:0,0,1第三行代表z轴的默认参数。在这个矩阵中,(a,b)是坐标轴的第i个底,(c,d)是第j个底,e是x轴的偏移量,f是y轴的偏移量;很容易理解,translate不会导致i和j的基数改变,只会移位,所以translate(-30px,-30px)==>matrix(1,0,0,1,30,30)~所有的变换语句,都会发生相应的变换,如下://发生偏移,但基向量不变;transform:translate(x,y)==>transform:matrix(1,0,0,1,x,y)//基向量旋转;变换:旋转(θdeg)==>变换:矩阵(cos(θ·π/180),sin(θ·π/180),-sin(θ·π/180),cos(θ·π/180),0,0)//基向量放大,方向不变;transform:scale(s)==>transform:matrix(s,0,0,s,0,0)translate/rotate/scale等语法非常强大,让我们的代码更易读,更方便编写,但是matrix有更多强大的变换特性,通过matrix,可以发生任何形式的变换,比如我们常见的镜像对称,transform:matrix(-1,0,0,1,0,0);MatrixTo但是,matrix虽然强大,它的可读性不好,我们写的是通过translate/rotate/scale的属性,但是getComputedStyle读取的transform是matrix:transform:matrix(1.41421,1.41421,-1.41421,1.41421,-50,-50);这个元素是如何改变的?.这是一脸茫然。-_-|||因此,我们必须要有一个方法将矩阵平移成我们比较熟悉的translate/rotate/scale方法。了解原理后,我们就可以开始执行了~我们知道,前4个参数会同时受到rotate和scale的影响,并且有两个变量,所以需要用前两个参数根据上面的转换列出两个不等式方法:cos(θ·π/180)*s=1.41421;sin(θ·π/180)*s=1.41421;将两个不等式相除,就可以轻松求出θ和s,完美!!函数如下:GesturePrinciple下面我们将在实际环境中使用上述函数,通过图文并茂的方式模拟手势的操作,并简单说明一下手势计算的原理。我希望在你理解了这些基本原理之后,你可以创造出更酷的手势,就像我们在mac触控板上使用的那样。下图说明:dot:代表手指的触摸点;两点之间的虚线段:表示双指操作时形成的向量;vectora/pointA:表示touchstart时得到的初始向量/初始点;bvector/B点:表示touchmove时获取的实时向量/实时点;坐标轴底部的公式表示要计算的值;上面的拖动(dragevent)是一个模拟的拖动手势,从A点移动到B点,我们要计算的是这个过程的偏移量;所以我们在touchstart中记录初始点A的坐标://获取初始点A;letstartPoint=getPoint(ev,0);然后获取touchmove事件点中的电流,实时计算△x和△y://实时获取初始点B;letcurPoint=getPoint(ev,0);//通过A、B两点,实时计算位移增量,触发拖动事件和Outgoing参数;_eventFire('drag',{delta:{deltaX:curPoint.x-startPoint.x,deltaY:curPoint.y-startPoint.y,},origin:ev,});提示:fire函数是遍历执行回调仓库对应拖动事件就够了;pinch(双指缩放)上图是双指缩放的模拟图,两根手指从向量a缩放到向量b,将初始状态下的向量a的模与touchmove中得到的b进行比较计算向量的模数得到缩放值://计算touchstart中初始两指的向量模数;letvector1=getVector(secondPoint,startPoint);letpinchStartLength=getLength(vector1);//计算touchmove测量模式下的实时两点;letvector2=getVector(curSecPoint,curPoint);letpinchLength=getLength(vector2);this._eventFire('pinch',{delta:{scale:pinchLength/pinchStartLength,},origin:ev,});Rotate(两指旋转)初始,双向向量a,旋转到b向量,θ就是我们需要的值,所以只要使用我们上面搭建的getAngle函数,就可以求出旋转角度://avector;letvector1=getVector(secondPoint,startPoint);//b向量;letvector2=getVector(curSecPoint,curPoint);//触发事件;this._eventFire('rotate',{delta:{rotate:getAngle(vector1,vector2),},origin:ev,});singlePinch(单指缩放)与上述手势不同。单指缩放和单指旋转都需要多个独特的概念:operator:需要操作的元素以上三种手势其实并不关心操作元素,因为正确的参数值仅靠手势本身就可以计算出来,而单指缩放和旋转则需要依赖于操作元素的参考点(中心操作元素的点)进行计算;按钮:因为单指手势和拖动手势相互冲突,所以需要特殊的交互方式来区分,这里是通过特定的区域来区分的,类似于按钮,当在按钮上操作,为单指缩放或旋转,在按钮区域外,为常规拖动。实践证明,这是一种用户容易接受、体验较好的操作方式;图中,用一根手指将a向量放大到b向量,运算符(正方形)的中心被放大,放大值是b向量的模数/a向量的模数;//计算单指操作时的参考点,获取操作者的中心点;letsingleBasePoint=getBasePoint(operator);//计算touchstart中的初始向量模式;letpinchV1=getVector(startPoint,singleBasePoint);singlePinchStartLength=getLength(pinchV1);//计算touchmove中的实时向量模式;pinchV2=getVector(curPoint,singleBasePoint);singlePinchLength=getLength(pinchV2);//触发事件;this._eventFire('singlePinch',{delta:{scale:singlePinchLength/singlePinchStartLength,},origin:ev,});singleRotate(单指旋转)结合了单指缩放和双指旋转,可以很容易的知道θ就是我们需要的旋转角度;//得到初始向量和实时向量letrotateV1=getVector(startPoint,singleBasePoint);letrotateV2=getVector(curPoint,singleBasePoint);//通过getAngle获取旋转角度并触发Event;this._eventFire('singleRotate',{delta:{rotate:getAngle(rotateV1,rotateV2),},origin:ev,});由于touchmove事件,移动增量是高频实时触发事件item,一次拖动操作实际上触发了N次touchmove事件,所以计算出来的值只是一个增量,即代表一次touchmove事件增加的值,只代表一个很小的值,不是最终值。结果值,所以需要通过mtouch.js对外维护一个位置数据,类似于//reallocationdata;letdragTrans={x=0,y=0};//累加mtouch传递的deltaX和deltaY;dragTrans.x+=ev.delta.deltaX;dragTrans.y+=ev.delta.deltaY;//直接通过transform操作元素;设置($拖动,dragTrans);初始位置维护的是外部位置数据,如果初始值像上面那样直接取0的话,会无法识别使用css设置了transform属性的元素,会导致操作元素跳转回(0,0)点开头,所以我们需要初步获取一个元素的Position值的真实值,然后进行维护和操作,此时需要用到我们上面提到的getComputedStyle方法和matrixTo函数://获取csstransform属性,此时获取的是一个矩阵数据;//transform:matrix(1.41421,1.41421,-1.41421,1.41421,-50,-50);letstyle=window.getComputedStyle(el,null);letcssTrans=style.transform||style.webkitTransform;//根据规则并得到:letinitTrans=_.matrixTo(cssTrans);//{x:-50,y:-50,scale:2,rotate:45};//即设置元素:transform:translate(-50px,-50px)缩放(2)旋转(45deg);结语至此,相信大家对手势的原理有了基本的了解。基于这些原则,我们可以封装更多的手势操作,比如双击、长按、滑动,甚至更炫酷的三指、四指操作等等,让应用具有更多的人性化特性。基于以上原理,我封装了几个常用的工具:(求星-.-)Tips:因为只针对移动端,所以需要在移动设备上打开demo,或者在手机上打开移动调试模式电脑端!mtouch.js:移动端手势库封装了以上五种手势,精简的api设计涵盖了常用的手势交互,基于此也可以轻松扩展。touchkit.js:基于mtouch封装的更贴近业务的工具包,可用于制作多种手势操作业务,一键开通,一站式服务。mcanvas.js:基于canvas开放最小化的API,实现一键导出图片等。感谢张新旭:获取元素CSS值的getComputedStyle方法熟悉张新旭:理解矩阵AlloyTeam团队的CSS3transform(matrix)AlloyFingerhcysunyangd:从矩阵和空间运算的关系理解CSS3transform学习了线性代数的理解后,感觉弱爆了
