一个最小手势库的实现

793 阅读4分钟
原文链接: www.cnblogs.com

众所周知,浏览器暴露了四个事件给开发者,touchstart touchmove touchend touchcancel,在这四个事件的回调函数可以拿到TouchEvent。TouchEvent:touches:当前位于屏幕上的所有手指动作的列表targetTouches:位于当前 DOM 元素上的手指动作的列表changedTouches:涉及当前事件的手指动作的列表

TouchEvent里可以拿到各个手指的坐标,那么可编程性就这么产生了。

Tap点按

移动端click有300毫秒延时,tap的本质其实就是touchend。但是要判断touchstart的手的坐标和touchend时候手的坐标x、y方向偏移要小于30。小于30才会去触发tap。

longTap长按

touchstart开启一个750毫秒的settimeout,如果750ms内有touchmove或者touchend都会清除掉该定时器。超过750ms没有touchmove或者touchend就会触发longTap

swipe划

这里需要注意,当touchstart的手的坐标和touchend时候手的坐标x、y方向偏移要大于30,判断swipe,小于30会判断tap。那么用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动呢?可以根据上面三个判断得出,具体的代码如下:

_swipeDirection: function (x1, x2, y1, y2) {
        return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
}

这个手势是使用频率非常高的,如图像裁剪的时候放大或者缩小图片,就需要pinch。

如上图所示,两点之间的距离比值求pinch的scale。这个scale会挂载在event上,让用户反馈给dom的transform或者其他元素的scale属性。

如上图所示,利用内积,可以求出两次手势状态之间的夹角θ。但是这里怎么求旋转方向呢?那么就要使用差乘(Vector Cross)。
利用cross结果的正负来判断旋转的方向。

所以,物理引擎里经常用cross来计算转动惯量,因为力矩其实要是力乘矩相当于面积:

主要的一些事件触发原理已经在上面讲解,还有如multipointStart、doubleTap、singleTap、multipointEnd可以看源码,不到200行的代码应该很容易消化。trigger手势事件的同时,touchStart、touchMove、touchEnd和touchCancel同样也可以监听。

/**
 * myHand.js
 */
 
"use strict";
 
(function(root, factory) {  
    if(typeof define === "function" && define.amd) {   //AMD规范
        define([], function() {
            return factory(root);
        });
    } else {
     root.myHand=root.Toucher = factory(root);   //把他挂载到window对象上
    }
}(window, function(root, undefined) {
 
    if(!"ontouchstart" in window) {
        return;
    }
 
    var _wrapped;
 
    //  获取对象上的类名
    function _typeOf(obj) {
        return Object.prototype.toString.call(obj).toLowerCase().slice(8, -1);
    }
 
    //  获取当前时间距1970/1/1时间戳
    function getTimeStr() {
        return +(new Date());
    }
 
    //  获取位置信息
    function getPosInfo(ev) {
        var _touches = ev.touches;
        if(!_touches || _touches.length === 0) {
            return;
        }
        return {
            pageX: ev.touches[0].pageX,
            pageY: ev.touches[0].pageY,
            clientX: ev.touches[0].clientX || 0,
            clientY: ev.touches[0].clientY || 0
        };
    }
 
    //  绑定事件
    function bindEv(el, type, fn) {
        if(el.addEventListener) {
            el.addEventListener(type, fn, false);
        } else {
            el["on" + type] = fn;
        }
    }
 
    //  解绑事件
    function unBindEv(el, type, fn) {
        if(el.removeEventListener) {
            el.removeEventListener(type, fn, false);
        } else {
            el["on" + type] = fn;
        }
    }
 
    //  获得滑动方向
    function getDirection(startX, startY, endX, endY) {
        var xRes = startX - endX;
        var xResAbs = Math.abs(startX - endX);
        var yRes = startY - endY;
        var yResAbs = Math.abs(startY - endY);
        var direction = "";
 
        if(xResAbs >= yResAbs && xResAbs > 25) {
            direction = (xRes > 0) ? "Right" : "Left";
        } else if(yResAbs > xResAbs && yResAbs > 25) {
            direction = (yRes > 0) ? "Down" : "Up";
        }
        return direction;
    }
 
    //  取得两点之间直线距离
    function getDistance(startX, startY, endX, endY) {
        return Math.sqrt(Math.pow((startX - endX), 2) + Math.pow((startY - endY), 2));
    }
 
    function getLength(pos) {
        return Math.sqrt(Math.pow(pos.x, 2) + Math.pow(pos.y, 2));
    }
 
    function cross(v1, v2) {
        return v1.x * v2.y - v2.x * v1.y;
    }
 
    //  取向量
    function getVector(startX, startY, endX, endY) {
        return(startX * endX) + (startY * endY);
    }
 
    //  获取角度  a*b=|a|*|b|*cos(deg);  a*b=x1*x2+y1*y2
    function getAngle(v1, v2) {
        var mr = getLength(v1) * getLength(v2);
        if(mr === 0) {
            return 0
        };
        var r = getVector(v1.x, v1.y, v2.x, v2.y) / mr;
        if(r > 1) {
            r = 1;
        }
        return Math.acos(r);
    }
 
    //  获取旋转的角度,不是弧度
    function getRotateAngle(v1, v2) {
        var angle = getAngle(v1, v2);
        if(cross(v1, v2) > 0) {
            angle *= -1;
        }
        return angle * 180 / Math.PI;
    }
 
    //  包装一个新的事件对象
    function wrapEvent(ev, obj) {
        var res = {
            touches: ev.touches,
            type: ev.type
        };
        if(_typeOf(obj) === "object") {
            for(var i in obj) {
                res[i] = obj[i];
            }
        }
        return res;
    }
 
    //  把伪数组转换成数组
    function toArray(list) {
        if(list && (typeof list === "object") && isFinite(list.length) && (list.length >= 0) && (list.length === Math.floor(list.length)) && list.length < 4294967296) {
            return [].slice.call(list);
        }
    }
 
    //  判断一个元素列表里面是否有多个元素
    function isContain(collection, el) {
        if(arguments.length === 2) {
            return collection.some(function(elItem) {
                return el.isEqualNode(elItem);
            });
        }
        return false;
    }
 
    //  生成一个随机id
    function uId() {
        return Math.random().toString(16).slice(2);
    }
 
    //  事件模块
    var Event = (function() {
 
        var storeEvents = {};
 
        return {
 
            //  add an event handle
            add: function(type, el, handler) {
                var selector = el,
                    len = arguments.length,
                    finalObject = {},
                    _type;
                /**
                 * Event.add("swipe", function() {
                 *      //  ...
                 * });
                 */
 
                if(_typeOf(el) === "string") {
                    el = document.querySelectorAll(el);
                }
 
                if(len === 2 && _typeOf(el) === "function") {
                    finalObject = {
                        handler: el
                    };
                } else if(len === 3 && el instanceof HTMLElement || el instanceof NodeList && _typeOf(handler) === "function") {
                    /**
                     * Event.add("swipe", "#div", function(ev) {
                     *      //  ...
                     * });
                     */
                    _type = _typeOf(el);
                    finalObject = {
                        type: _type,
                        selector: selector,
                        el: _type === "nodelist" ? toArray(el) : el,
                        handler: handler
                    };
                }
 
                if(!storeEvents[type]) {
                    storeEvents[type] = [];
                }
 
                storeEvents[type].push(finalObject);
            },
 
            //  remove an event handle
            remove: function(type, selector) {
                var len = arguments.length;
                if(_typeOf(type) === "string" && _typeOf(storeEvents[type]) === "array" && storeEvents[type].length) {
                    if(len === 1) {
                        storeEvents[type] = [];
                    } else if(len === 2) {
                        storeEvents[type] = storeEvents[type].filter(function(item) {
                            return !(item.selector === selector || _typeOf(selector) !== "string" && item.selector.isEqualNode(selector));
                        });
                    }
                }
            },
 
            //  trigger an event handle
            trigger: function(type, el, argument) {
                var len = arguments.length;
 
                /**
                 * Event.trigger("swipe", document.querySelector("#div"), {
                 *      //  ...
                 * });
                 */
                if(len === 3 && _typeOf(storeEvents[type]) === "array" && storeEvents[type].length) {
                    storeEvents[type].forEach(function(item) {
                        if(_typeOf(item.handler) === "function") {
                            if(item.type && item.el) {
                                argument.target = el;
                                if(item.type === "nodelist" && isContain(item.el, el)) {
                                    item.handler(argument);
                                } else if(item.el.isEqualNode && item.el.isEqualNode(el)) {
                                    item.handler(argument);
                                }
                            } else {
                                item.handler(argument);
                            }
                        }
                    });
                }
            }
        };
    })();
 
    //  构造函数
    function Toucher(selector) {
        return new Toucher.fn.init(selector);
    }
 
    Toucher.fn = Toucher.prototype = {
 
        //  修改原型构造器
        constructor: Toucher,
 
        //  初始化方法
        init: function(selector) {
            this.el = selector instanceof HTMLElement ? selector :
                _typeOf(selector) === "string" ? document.querySelector(selector) : null;
            if(_typeOf(this.el) === "null") { //如果没有匹配到
                throw new Error("您必须指定一个特定的选择器或特定的DOM对象");
            }
            this.scale = 1;
            this.pinchStartLen = null;
            this.isDoubleTap = false;
            this.triggedSwipeStart = false;
            this.triggedLongTap = false;
            this.delta = null;
            this.last = null;
            this.now = null;
            this.tapTimeout = null;
            this.singleTapTimeout = null;
            this.longTapTimeout = null;
            this.swipeTimeout = null;
            this.startPos = {};
            this.endPos = {};
            this.preTapPosition = {};
 
            this.cfg = {
                doubleTapTime: 400,
                longTapTime: 700
            };
 
            //  绑定4个事件
            bindEv(this.el, "touchstart", this._start.bind(this));
            bindEv(this.el, "touchmove", this._move.bind(this));
            bindEv(this.el, "touchcancel", this._cancel.bind(this));
            bindEv(this.el, "touchend", this._end.bind(this));
            return this;
        },
 
        //  提供config方法进行配置
        config: function(option) {
            if(_typeOf(option) !== "object") {
                throw new Error("option 必须是一个JSON的实例对象" + option.toString());
            }
            for(var i in option) {
                this.cfg[i] = option[i];
            }
            return this;
        },
 
        //  on方法绑定事件
        /**
         * var toucher = Toucher({...});
         *
         * toucher.on("swipe", function(ev) {
         *     //   ...
         * });
         *
         * //   or
         *
         * toucher.on("tap", "#id", function(ev) {
         *     //   ...
         * });
         *
         * support events: singleTap,longTap,swipe,swipeStart,swipeEnd,swipeUp,swipeRight,swipeDown,swipeLeft,pinch,rotate
         *
         */
 
        on: function(type, el, callback) {
            var len = arguments.length;
            if(len === 2) {
                Event.add(type, el);
            } else {
                Event.add(type, el, callback);
            }
            return this;
        },
 
        //  off 解除绑定
        /**
         *  var toucher = Toucher({...});
         *  toucher.off(type);
         *
         *  //  or
         *
         *  toucher.off(type, selector);
         */
        off: function(type, selector) {
            Event.remove(type, selector);
            return this;
        },
 
        //  手指刚触碰到屏幕
        _start: function(ev) {
            if(!ev.touches || ev.touches.length === 0) {
                return;
            }
 
            var self = this;
            var otherToucher, v,
                preV = this.preV,
                target = ev.target; //获取目标元素
 
            self.now = getTimeStr();  //获取当前时间距1970/1/1时间戳
            self.startPos = getPosInfo(ev);  //获取点击的坐标位置信息
            self.delta = self.now - (self.last || self.now); //计算时间间隔
            self.triggedSwipeStart = false;
            self.triggedLongTap = false;
 
            //  快速双击
            if(JSON.stringify(self.preTapPosition).length > 2 && self.delta < self.cfg.doubleTapTime && getDistance(self.preTapPosition.clientX, self.preTapPosition.clientY, self.startPos.clientX, self.startPos.clientY) < 25) {
                //第一次点击保存了信息内容长度>2,双击时间间隔小于400,两次点击的两点之间直线距离小于半径25的圆圈内
                self.isDoubleTap = true;
            }
 
            //  长按定时
            self.longTapTimeout = setTimeout(function() {
                _wrapped = {
                    el: self.el,
                    type: "longTap",
                    timeStr: getTimeStr(),
                    position: self.startPos
                };
                Event.trigger("longTap", target, _wrapped);
                self.triggedLongTap = true;
            }, self.cfg.longTapTime);
 
            //  多个手指放到屏幕
            if(ev.touches.length > 1) {
                self._cancelLongTap();
                otherToucher = ev.touches[1];
                v = {
                    x: otherToucher.pageX - self.startPos.pageX,
                    y: otherToucher.pageY - self.startPos.pageY
                };
                this.preV = v;
                self.pinchStartLen = getLength(v);
                self.isDoubleTap = false;
            }
 
            self.last = self.now;
            self.preTapPosition = self.startPos;  //保存上一次点击的坐标位置信息
 
            ev.preventDefault();
        },
 
        //  手指在屏幕上移动
        _move: function(ev) {
            if(!ev.touches || ev.touches.length === 0) {
                return;
            }
 
            var v, otherToucher;
            var self = this;
            var len = ev.touches.length;
            var posNow = getPosInfo(ev);
            var preV = self.preV;
            var currentX = posNow.pageX;
            var currentY = posNow.pageY;
            var target = ev.target;
 
            //  手指移动取消长按事件和双击
            self._cancelLongTap();
            self.isDoubleTap = false;
 
            //  一次按下抬起只触发一次swipeStart
            if(!self.triggedSwipeStart) {
                _wrapped = {
                    el: self.el,
                    type: "swipeStart",
                    timeStr: getTimeStr(),
                    position: posNow
                };
                Event.trigger("swipeStart", target, _wrapped);
                self.triggedSwipeStart = true;
            } else {
                _wrapped = {
                    el: self.el,
                    type: "swipe",
                    timeStr: getTimeStr(),
                    position: posNow
                };
                Event.trigger("swipe", target, _wrapped);
            }
 
            if(len > 1) {
                otherToucher = ev.touches[1];
                v = {
                    x: otherToucher.pageX - currentX,
                    y: otherToucher.pageY - currentY
                };
 
                //  缩放
                _wrapped = wrapEvent(ev, {
                    el: self.el,
                    type: "pinch",
                    scale: getLength(v) / this.pinchStartLen,
                    timeStr: getTimeStr(),
                    position: posNow
                });
                Event.trigger("pinch", target, _wrapped);
 
                //  旋转
                _wrapped = wrapEvent(ev, {
                    el: self.el,
                    type: "rotate",
                    angle: getRotateAngle(v, preV),
                    timeStr: getTimeStr(),
                    position: posNow
                });
                Event.trigger("rotate", target, _wrapped);
                ev.preventDefault();
            }
 
            self.endPos = posNow;
        },
 
        //  触碰取消
        _cancel: function(ev) {
            clearTimeout(this.longTapTimeout);
            clearTimeout(this.tapTimeout);
            clearTimeout(this.swipeTimeout);
            clearTimeout(self.singleTapTimeout);
        },
 
        //  手指从屏幕离开
        _end: function(ev) {
            if(!ev.changedTouches) {
                return;
            }
 
            //  取消长按
            this._cancelLongTap();
 
            var self = this;
            var direction = getDirection(self.endPos.clientX, self.endPos.clientY, self.startPos.clientX, self.startPos.clientY);
            var callback, target = ev.target;
 
            if(direction !== "") {
                self.swipeTimeout = setTimeout(function() {
                    _wrapped = wrapEvent(ev, {
                        el: self.el,
                        type: "swipe",
                        timeStr: getTimeStr(),
                        position: self.endPos
                    });
                    Event.trigger("swipe", target, _wrapped);
 
                    //  获取具体的swipeXyz方向
                    callback = self["swipe" + direction];
                    _wrapped = wrapEvent(ev, {
                        el: self.el,
                        type: "swipe" + direction,
                        timeStr: getTimeStr(),
                        position: self.endPos
                    });
                    Event.trigger(("swipe" + direction), target, _wrapped);
 
                    _wrapped = wrapEvent(ev, {
                        el: self.el,
                        type: "swipeEnd",
                        timeStr: getTimeStr(),
                        position: self.endPos
                    });
                    Event.trigger("swipeEnd", target, _wrapped);
                }, 0);
            } else if(!self.triggedLongTap) {
                self.tapTimeout = setTimeout(function() {
                    if(self.isDoubleTap) {
                        _wrapped = wrapEvent(ev, {
                            el: self.el,
                            type: "doubleTap",
                            timeStr: getTimeStr(),
                            position: self.startPos
                        });
                        Event.trigger("doubleTap", target, _wrapped);
                        clearTimeout(self.singleTapTimeout);
                        self.isDoubleTap = false;
                    } else {
                        self.singleTapTimeout = setTimeout(function() {
                            _wrapped = wrapEvent(ev, {
                                el: self.el,
                                type: "singleTap",
                                timeStr: getTimeStr(),
                                position: self.startPos
                            });
                            Event.trigger("singleTap", target, _wrapped);
                        }, 100);
                    }
                }, 0);
            }
 
            this.startPos = {};
            this.endPos = {};
        },
 
        //  取消长按定时器
        _cancelLongTap: function() {
            if(_typeOf(this.longTapTimeout) !== "null") {
                clearTimeout(this.longTapTimeout);
            }
        }
    };
 
    Toucher.fn.init.prototype = Toucher.fn;  //无new 实现
 
    return Toucher;
 
}));


 
    
        
        
        
        
            * {
                margin: 0;
                padding: 0;
            }
             
            #toucher {
                width: 100%;
                height: 400px;
                background: yellow;
            }
        
    
 
    
        

部分奇葩手机不支持e.touches,可加在上面最上面库文件的36行处:

 // touches
function fnTouches(e) {
    if(!e.touches) {
        e.touches = e.originalEvent.touches;
    }
}