话说上周周末闲的蛋疼,忽然想了解一下前端手势如何处理,好解开本身一个知识盲点,因而开始啃源码。。。并纪录一下。javascript
在咱们的前端页面里面复杂的手势应该是很少见的,通常经常使用就是拖拉,双击,放大缩小这几个,可是合理运用手势很明显也能为咱们页面的交互体验有一点增色,那么问题来了,如何识别一个手势尼?css
Hammer.js 应该算是前端使用的比较普遍的一个手势框架了(我所了解的还有一个AlloyTouch,更小,固然它提供的抽象程度是不如Hammer.js的),今天就拿这个框架来开刀吧。前端
咱们先来看Hammer.js的配置参数:java
{ //手势事件触发时,是否同时触发对应的一个自定义的dom事件,固然这个没有直接绑定事件回调高效 domEvents: false, //这个会影响对应的css属性touch-action的值,下面会接着说 touchAction: TOUCH_ACTION_COMPUTE, enable: true, //是否开启手势识别 //能够指定在其余的元素上来检测与touch相关的事件并做为输入源,若是没设置就是当前检测的元素了 inputTarget: null, inputClass: null, //输入源类型,鼠标仍是触摸或者是混合 recognizers: [], //咱们配置的手势识别器 //预设的一些手势识别器,格式:[RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]] preset: [ [RotateRecognizer, { enable: false }], [PinchRecognizer, { enable: false }, ['rotate']], [SwipeRecognizer, { direction: DIRECTION_HORIZONTAL }], [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], [TapRecognizer], [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], [PressRecognizer] ], cssProps: { //额外的一些css属性 userSelect: 'none', touchSelect: 'none', touchCallout: 'none', contentZooming: 'none', userDrag: 'none', tapHighlightColor: 'rgba(0,0,0,0)' } }
总的来讲配置参数很少,也不算复杂,这个框架基本也算是开箱即用了,好,咱们接着再深刻一点。浏览器
接着来到源码里面manager.js,能够看到如下一段的代码:session
export default class Manager { constructor() { ... this.element = element; this.input = createInputInstance(this);// 1 this.touchAction = new TouchAction(this,this.options.touchAction);// 2 toggleCssProps(this, true); each(this.options.recognizers, (item) => { //3 let recognizer = this.add(new (item[0])(item[1])); item[2] && recognizer.recognizeWith(item[2]); item[3] && recognizer.requireFailure(item[3]); }, this); } ... }
1.新建一个输入源
根据设备的不一样手势多是来自鼠标也有可能来自手机上的触摸屏,并且mouse event的属性和touch event的属性是有一丝差别的(还有pointer event),因此为了方便后续处理,Hammer.js也分别定义了不一样类型输入源:MouseInput,PointerEventInput,SingleTouchInput,TouchInput和TouchMouseInput;并针对不一样的事件,对参数作了一个简单处理(handler方法),最终获得统一格式的数据输出,就像这样:框架
{ pointers: touches[0], changedPointers: touches[1], pointerType: INPUT_TYPE_TOUCH, srcEvent: ev }
在获取统一格式的输入数据后,会交由InputHandler进一步处理,会判断此次输入是手势的开始仍是结束,若是是开始就会新建一个手势识别的session,而且计算一些与手势相关的数据(角度,偏移距离,移动方向等),具体能够在compute-input-data.js里面看到。
通过以这一轮计算,咱们已经有足够的数据来支持以后的手势识别了。
另一提的是,这五种输入源都继承了Input,在Input里面事件是这样绑定的:dom
this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
有三种绑定目标,当前的element,inputTarget,element所属的window,在window上绑定事件处理器仍是很必要的(例如拖拉一个元素的时候);另外翻了一下代码,inputTarget绑定都是touch相关的事件,不是很明白它的意图和场景,为何要分离一个目标单独处理触摸事件。性能
2.设置元素样式里touch-action的值
在手机浏览器里面,通常也会自带一些手势处理,例如向右滑动或者向左滑动就是前进和后退,因此除了咱们本身定义手势,还须要对浏览器的手势作一些限制或者禁止。
这里也举个栗子吧,在Hammer.js里面默认提供拖拉手势的识别器(就是pan.js),当在检测水平方向的拖拉的时候,这个识别器会把touch-action的值设为pay-y(容许浏览器处理垂直方向的拖拉,能够是一个垂直的滚动或者其余),那若是我又接着定义一个垂直方向拖拉的识别器时,touch-action的值是多少尼?(答案就是none,浏览器不会帮咱们再处理了,垂直方向滚动也只能靠本身),那是怎样计算出来的尼?ui
在建立TouchAction对象时,若是配置参数中touchAction的值为TOUCH_ACTION_COMPUTE,便调用compute方法开始遍历recognizers,收集它们所但愿设置的touch-action的值:
compute() { let actions = []; each(this.manager.recognizers, (recognizer) => { if (boolOrFn(recognizer.options.enable, [recognizer])) { actions = actions.concat(recognizer.getTouchAction()); } }); return cleanTouchActions(actions.join(' ')); }
最终在cleanTouchActions方法集中计算最终的值:
... let hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); let hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); if (hasPanX && hasPanY) { return TOUCH_ACTION_NONE; } ...
3.配置手势识别器
主要是配置各个手势识别器之间的关系,是否能够协同仍是互斥,用官网一个例子:
var hammer = new Hammer(el, {}); var singleTap = new Hammer.Tap({ event: 'singletap' }); var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2 }); var tripleTap = new Hammer.Tap({event: 'tripletap', taps: 3 }); hammer.add([doubleTap, doubleTap, singleTap]); tripleTap.recognizeWith([doubleTap, singleTap]); doubleTap.recognizeWith(singleTap); doubleTap.requireFailure(tripleTap); singleTap.requireFailure([tripleTap, doubleTap]);
以上定义了三个手势识别器:singleTap,doubleTap和tripleTap,很明显这个三个识别器是互斥的,若是用户点三下屏幕时都触发就比较尴尬了;
这里得注意添加的顺序,由于Hammer.js是会按顺序遍历识别器调用他们的recognize方法,由于咱们已经设置了手势的互斥,Hammer.js为了知道手势是单击仍是双击,singleTap,doubleTap,tripleTap识别器都设置了300ms等待时间来判断以后还会不会有点击事件,根据识别顺序,singleTap总能获取tripleTap和doubleTap的识别结果来判断是否要触发事件,假如咱们不设置他们之间的互斥关系,Hammer.js默认一知足条件就会触发,就会出现刚才说的那种尴尬的场景。
那recognizeWith有啥做用尼,看如下代码:
if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { curRecognizer = session.curRecognizer = null; } let i = 0; while (i < recognizers.length) { recognizer = recognizers[i]; if (session.stopped !== FORCED_STOP && ( !curRecognizer || recognizer === curRecognizer || recognizer.canRecognizeWith(curRecognizer))) { recognizer.recognize(inputData); } else { recognizer.reset(); } if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { curRecognizer = session.curRecognizer = recognizer; } i++; }
虽然singleTap,doubleTap和tripleTap从最终结果上应该是互斥的,可是一样的数据输入时可能会同时让几个手势识别器识别,例如当用户点击一下屏幕,singleTap识别器的状态多是STATE_RECOGNIZED或者STATE_BEGAN(等待doubleTap和tripleTap识别器的结果),session会把singTap识别器记录为当前的手势识别器,可是doubleTap和tripleTap也是须要记录一些状态(例如当前点击次数),由于颇有可能接下来又是一个单击,变成双击手势;当用户接着再单击一下,doubleTap识别器由于设置了recognizeWith(singleTap)和以协同singleTap识别数据输入,而后doubleTap识别器开始进入STATE_RECOGNIZED或者STATE_BEGAN(等待tripleTap识别器的结果),此时session当前的手势识别器就是doubleTap了,而singleTap识别器由于没有设置recognizeWith(doubleTap),会被重置。
咱们在旋转一张图片时,如何实现旋转,怎么知道旋转的角度尼?
再回到computeInputData方法,有这样一行代码获取偏转角度:
... let center = input.center = getCenter(pointers); ... input.angle = getAngle(offsetCenter, center); ...
再跟踪一下getCenter方法:
while (i < pointersLength) { x += pointers[i].clientX; y += pointers[i].clientY; i++; } return { x: round(x / pointersLength), y: round(y / pointersLength) };
很简单的算出手势的中心位置,当咱们双指旋转时,中心位置也会跟着移动,很容易计算出先后偏转角度。
Hammer.js都是在冒泡阶段绑定事件处理器,为何不在捕获阶段拦截事件尼,若是一个向右活动的手势被识别,后续的事件(如touchMove)已经不必再传给子节点,彻底能够在拦截的元素上处理,这样性能上也应该会有一点提高,挖个坑给本身之后实现一下。最后的最后。。。由于没有使用经验,单靠啃源码,不免有所错漏,望指正。