UNPKG

zingfinger

Version:

super tiny size multi-touch gestures library for the web

490 lines (439 loc) 16.6 kB
//利用勾股定理公式就可以很容易的计算出斜边的长度,也就是手指触点之间的直线距离 const getLen = (v) => { return Math.sqrt(v.x * v.x + v.y * v.y); }; const dot = (v1, v2) => { return v1.x * v2.x + v1.y * v2.y; }; // 计算弧度 const getAngle = (v1, v2) => { let mr = getLen(v1) * getLen(v2); if (mr === 0) return 0; let r = dot(v1, v2) / mr; if (r > 1) r = 1; return Math.acos(r); }; //利用cross结果的正负来判断旋转的方向,如果值大于0,表示方向是逆时针,值小于0,表示方向顺时针 const cross = (v1, v2) => { return v1.x * v2.y - v2.x * v1.y; }; //利用数学向量求出旋转角度 const getRotateAngle = (v1, v2) => { let angle = getAngle(v1, v2); if (cross(v1, v2) > 0) { angle *= -1; } return (angle * 180) / Math.PI; }; const getOffset = (el) => { let rect = el.getBoundingClientRect(); let offset = { left: rect.left + document.body.scrollLeft, top: rect.top + document.body.scrollTop, width: el.offsetWidth, height: el.offsetHeight, }; return offset; }; const getMidpoint = (el) => { if (!el) return { x: 0, y: 0 }; let offset = getOffset(el); let x = offset.left + el.getBoundingClientRect().width / 2, y = offset.top + el.getBoundingClientRect().width / 2; return { x: Math.round(x), y: Math.round(y) }; }; class HandlerAdmin { constructor(el) { this.handlers = []; this.el = el; } add(handler) { this.handlers.push(handler); } del(handler) { if (!handler) this.handlers = []; for (let i = this.handlers.length; i >= 0; i--) { if (this.handlers[i] === handler) { this.handlers.splice(i, 1); } } } dispatch() { for (let i = 0, len = this.handlers.length; i < len; i++) { let handler = this.handlers[i]; if (typeof handler === "function") handler.apply(this.el, arguments); } } } const wrapEl = (el) => { return typeof el == "string" ? document.querySelector(el) : el; }; const wrapFunc = (el, handler) => { const handlerAdmin = new HandlerAdmin(el); handlerAdmin.add(handler); return handlerAdmin; }; const isDescendant = (parent, child) => { let node = child.parentNode; while (node != null) { if (node == parent) { return true; } node = node.parentNode; } return false; }; export default class ZingFinger { //option是个数据对象,包含了所有的操作回调函数 constructor(el, option, handleEl) { this.element = wrapEl(el); this.handleEl = wrapEl(handleEl); this.start = this.start.bind(this); this.move = this.move.bind(this); this.end = this.end.bind(this); this.cancel = this.cancel.bind(this); //监听touch事件 this.element.addEventListener("touchstart", this.start, false); this.element.addEventListener("touchmove", this.move, false); this.element.addEventListener("touchend", this.end, false); this.element.addEventListener("touchcancel", this.cancel, false); //存储两个手指触摸点的位置的间距,水平间距和垂直间距 this.preV = { x: null, y: null }; //存储多指触摸操作时,手指触点位置之间的距离 this.pinchStartLen = null; this.zoom = 1; this.isDoubleTap = false; let noop = function () {}; //旋转操作(多指旋转操作) this.rotate = wrapFunc(this.element, option.rotate || noop); //旋转操作 (单指) this.singleRotate = wrapFunc(this.handleEl, option.singleRotate || noop); //手指触摸开始 this.touchStart = wrapFunc(this.element, option.touchStart || noop); //多指触摸开始 this.multipointStart = wrapFunc( this.element, option.multipointStart || noop ); //多指触摸结束 this.multipointEnd = wrapFunc(this.element, option.multipointEnd || noop); //捏(缩放操作) this.pinch = wrapFunc(this.element, option.pinch || noop); //单指缩放操作 this.singlePinch = wrapFunc(this.handleEl, option.singlePinch || noop); //手指划过操作(兼容单个手指操作,多个手指操作) this.swipe = wrapFunc(this.element, option.swipe || noop); //点击操作 this.tap = wrapFunc(this.element, option.tap || noop); //双击操作 this.doubleTap = wrapFunc(this.element, option.doubleTap || noop); //长按操作 this.longTap = wrapFunc(this.element, option.longTap || noop); //点击操作 //tap操作和singleTap操作的区别在于,如果是在一定时间内只是单击一次的话,触发的操作顺序是tap->singleTap //singleTap操作其实就类似于鼠标click事件,click事件作用到移动端页面的时候,会存在延时触发事件,会先触发touch事件再执行click事件 this.singleTap = wrapFunc(this.element, option.singleTap || noop); //单个手指触摸滑动操作 this.pressMove = wrapFunc(this.element, option.pressMove || noop); //两个手指触摸滑动操作 this.twoFingerPressMove = wrapFunc( this.element, option.twoFingerPressMove || noop ); //触摸滑动 this.touchMove = wrapFunc(this.element, option.touchMove || noop); //触摸结束,手指触点离开屏幕 this.touchEnd = wrapFunc(this.element, option.touchEnd || noop); //系统原因中断手势操作 this.touchCancel = wrapFunc(this.element, option.touchCancel || noop); this._cancelAllHandler = this.cancelAll.bind(this); window.addEventListener("scroll", this._cancelAllHandler); //手指连续按下触摸操作之间的时间间隔 this.delta = null; //手指最近一次按下触摸操作时的时间戳 this.last = null; //手指按下触摸操作的时间戳 this.now = null; //接收点击操作时的定时器返回的值,用于清除定时器 this.tapTimeout = null; //接收点击操作时的定时器返回的值,用于清除定时器 this.singleTapTimeout = null; //接收长按操作时的定时器返回的值,用于清除定时器 this.longTapTimeout = null; this.swipeTimeout = null; this.x1 = this.x2 = this.y1 = this.y2 = null; //用于存储手指触摸操作时的水平坐标和垂直坐标(如果是多指触摸操作,则记录的是第一个手指触摸的位置) this.preTapPosition = { x: null, y: null }; } start(evt) { if (!evt.touches) return; this.now = Date.now(); //存储手指触点相对于HTML文档左边沿的的X坐标 this.x1 = evt.touches[0].pageX; //存储手指触点相对于HTML文档上边沿的的Y坐标 this.y1 = evt.touches[0].pageY; //计算出手指连续按下触摸操作之间的时间间隔 this.delta = this.now - (this.last || this.now); this.touchStart.dispatch(evt, this.element); if (this.preTapPosition.x !== null) { //如果手指连续触摸操作之间的时间间隔小于250毫秒,且手指连续触摸操作之间的触点位置水平坐标小于30, //垂直坐标小于30,那么就判定该操作为双击操作 this.isDoubleTap = this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30; if (this.isDoubleTap) clearTimeout(this.singleTapTimeout); } this.preTapPosition.x = this.x1; this.preTapPosition.y = this.y1; this.last = this.now; //获取触摸点的数量 let preV = this.preV, len = evt.touches.length; if (len > 1) { this._cancelLongTap(); this._cancelSingleTap(); //如果是多手指操作的,计算出手指触摸点的位置的间距,水平间距和垂直间距 let v = { x: evt.touches[1].pageX - this.x1, y: evt.touches[1].pageY - this.y1, }; preV.x = v.x; preV.y = v.y; //存储手指触点间的直线距离 this.pinchStartLen = getLen(preV); this.multipointStart.dispatch(evt, this.element); } //判断操作是单击还是长按,true为长按操作,false为单击操作 this._preventTap = false; //长按时的操作,长按时间750毫秒才会执行 this.longTapTimeout = setTimeout( function () { this.longTap.dispatch(evt, this.element); this._preventTap = true; }.bind(this), 750 ); } move(evt) { if (!evt.touches) return; let preV = this.preV, len = evt.touches.length, currentX = evt.touches[0].pageX, currentY = evt.touches[0].pageY; //手指滑动的时候,就可以判定当前的操作不是双击了,所以把双击操作的状态设为false this.isDoubleTap = false; //多个手指操作 if (len > 1) { let sCurrentX = evt.touches[1].pageX, sCurrentY = evt.touches[1].pageY; let v = { x: evt.touches[1].pageX - currentX, y: evt.touches[1].pageY - currentY, }; //多指操作时,且触摸点位置的间距存在,也就是preV.x或者preV.y存在的时候才能执行pinch操作,这个判断条件必须存在 //(因为可能存在当多个手指触摸屏幕时,那么存在多个触摸点,但是在滑动操作的同时,只保留了一个手指触摸点,其他手指移开屏幕这样的情况,这种情况就不能执行pinch操作 if (preV.x !== null) { if (this.pinchStartLen > 0) { //计算出缩放比例(当前手指触摸点的直线距离 / 上一次滑动之前的手指触摸点的直线距离) // console.log(this.pinchStartLen); evt.zoom = getLen(v) / this.pinchStartLen; this.pinch.dispatch(evt, this.element); } //旋转手势操作 evt.angle = getRotateAngle(v, preV); this.rotate.dispatch(evt, this.element); } preV.x = v.x; preV.y = v.y; if (this.x2 !== null && this.sx2 !== null) { evt.deltaX = (currentX - this.x2 + sCurrentX - this.sx2) / 2; evt.deltaY = (currentY - this.y2 + sCurrentY - this.sy2) / 2; } else { evt.deltaX = 0; evt.deltaY = 0; } this.twoFingerPressMove.dispatch(evt, this.element); //存储在移动操作时第二个手指的X坐标位置 this.sx2 = sCurrentX; //存储在移动操作时第二个手指的Y坐标位置 this.sy2 = sCurrentY; } else { if (this.x2 !== null) { evt.deltaX = currentX - this.x2; evt.deltaY = currentY - this.y2; //move事件中添加对当前触摸点到初始触摸点的判断, //如果曾经大于过某个距离(比如10),就认为是移动到某个地方又移回来,应该不再触发tap事件才对。 let movedX = Math.abs(this.x1 - this.x2), movedY = Math.abs(this.y1 - this.y2); if (movedX > 10 || movedY > 10) { this._preventTap = true; } } else { evt.deltaX = 0; evt.deltaY = 0; } this.pressMove.dispatch(evt, this.element); // 判断是否有handleElement if ( this.handleEl && isDescendant(this.element, this.handleEl) && this.handleEl.dataset.single == "true" && evt.target == this.handleEl ) { const basePoint = getMidpoint(this.element); const startV = { x: this.handleEl.offsetLeft - this.handleEl.offsetWidth / 4, y: this.handleEl.offsetTop - this.handleEl.offsetHeight / 4, }; const rectV = { x: currentX - startV.x, y: currentY - startV.y, }; preV = { x: rectV.x - this.x1, y: rectV.y - this.y1, }; this.pinchStartLen = getLen(preV); evt.zoom = getLen(rectV) / this.pinchStartLen; // 单指缩放 this.singlePinch.dispatch(evt, this.handleEl); // 单指旋转 const rotateV2 = { x: basePoint.x - currentX, y: basePoint.y - currentY, }; evt.angle = getRotateAngle(rotateV2, preV); preV.x = rotateV2.x; preV.y = rotateV2.y; this.singleRotate.dispatch(evt, this.handleEl); // 是否为在handleElement上,禁止touch与屏幕滑动的冲突 evt.preventDefault(); } } this.touchMove.dispatch(evt, this.element); this._cancelLongTap(); this.x2 = currentX; this.y2 = currentY; if (len > 1) { evt.preventDefault(); } } end(evt) { if (!evt.changedTouches) return; //清除长按操作的定时器,取消长按操作 this._cancelLongTap(); let self = this; if (evt.touches.length < 2) { this.multipointEnd.dispatch(evt, this.element); this.sx2 = this.sy2 = null; } //swipe //水平移动间距大于30或者垂直移动间距大于30,就判定为swipe操作 if ( (this.x2 && Math.abs(this.x1 - this.x2) > 30) || (this.y2 && Math.abs(this.y1 - this.y2) > 30) ) { evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2); this.swipeTimeout = setTimeout(function () { self.swipe.dispatch(evt, self.element); }, 0); } else { this.tapTimeout = setTimeout(function () { if (!self._preventTap) { self.tap.dispatch(evt, self.element); } // trigger double tap immediately // 如果是双击操作的话,就执行 if (self.isDoubleTap) { self.doubleTap.dispatch(evt, self.element); self.isDoubleTap = false; } }, 0); //如果不是双击操作,就会延时250毫秒执行singleTap操作,同时也会执行tap操作,执行顺序是tap->singleTap if (!self.isDoubleTap) { self.singleTapTimeout = setTimeout(function () { self.singleTap.dispatch(evt, self.element); }, 250); } } this.touchEnd.dispatch(evt, this.element); this.preV.x = 0; this.preV.y = 0; this.zoom = 1; this.pinchStartLen = null; this.x1 = this.x2 = this.y1 = this.y2 = null; } cancelAll() { this._preventTap = true; clearTimeout(this.singleTapTimeout); clearTimeout(this.tapTimeout); clearTimeout(this.longTapTimeout); clearTimeout(this.swipeTimeout); } cancel(evt) { this.cancelAll(); this.touchCancel.dispatch(evt, this.element); } _cancelLongTap() { clearTimeout(this.longTapTimeout); } _cancelSingleTap() { clearTimeout(this.singleTapTimeout); } //判定swipe滑动的方向 _swipeDirection(x1, x2, y1, y2) { return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? x1 - x2 > 0 ? "Left" : "Right" : y1 - y2 > 0 ? "Up" : "Down"; } on(evt, handler) { if (this[evt]) { this[evt].add(handler); } } off(evt, handler) { if (this[evt]) { this[evt].del(handler); } } destroy() { //清除所有的定时器操作 if (this.singleTapTimeout) clearTimeout(this.singleTapTimeout); if (this.tapTimeout) clearTimeout(this.tapTimeout); if (this.longTapTimeout) clearTimeout(this.longTapTimeout); if (this.swipeTimeout) clearTimeout(this.swipeTimeout); //解绑所有的监听事件 this.element.removeEventListener("touchstart", this.start); this.element.removeEventListener("touchmove", this.move); this.element.removeEventListener("touchend", this.end); this.element.removeEventListener("touchcancel", this.cancel); //取消所有的订阅 this.rotate.del(); this.singleRotate.del(); this.touchStart.del(); this.multipointStart.del(); this.multipointEnd.del(); this.pinch.del(); ths.singlePinch.del(); this.swipe.del(); this.tap.del(); this.doubleTap.del(); this.longTap.del(); this.singleTap.del(); this.pressMove.del(); this.twoFingerPressMove.del(); this.touchMove.del(); this.touchEnd.del(); this.touchCancel.del(); //自空所有的数据 this.preV = this.pinchStartLen = this.zoom = this.isDoubleTap = this.delta = this.last = this.now = this.tapTimeout = this.singleTapTimeout = this.longTapTimeout = this.swipeTimeout = this.x1 = this.x2 = this.y1 = this.y2 = this.preTapPosition = this.rotate = this.singleRotate = this.touchStart = this.multipointStart = this.multipointEnd = this.pinch = this.singlePinch = this.swipe = this.tap = this.doubleTap = this.longTap = this.singleTap = this.pressMove = this.touchMove = this.touchEnd = this.touchCancel = this.twoFingerPressMove = null; window.removeEventListener("scroll", this._cancelAllHandler); return null; } }