javascript 常用手势 分析

javascript 常用手势, 个人觉得有3个 tap,swipe(swipeLeft,swipeRight,swipeTop,swipeRight),hold

tap 是轻击 判断的原则是,在toustart后,移动范围不超过10px(圆的范围),就算是 轻击了

swipe 是轻滑(轻扫) 判断在toustart后,时间间隔小于300ms,移动范围大于20,就判断是轻滑

hold(常按) 按住 移动范围小于10px,时间大于200ms,就认为他是hold


自定义手势,网上相关的源码很多,我也找了一个来研究,叫touch.js,挺不错的支持pc端,移动端(移动端就是touchstart,pc端就是mousedown),虽然有些小bug,比如事件删除有问题

touch.js的地址 http://touch.code.baidu.com/


一些要准备的基础

手势的基本实现原理

阉割源码解析

支持移动端 pc端的阉割源码解析

zepto的手势源码解析

一些我遇到的手势问题


一些要准备的基础

1.对touch相关的东西要了解

指尖上的js是很好的东西呀

指尖上的js一

指尖上的js二

指尖上的js三


2.自定义事件CustomEvent

dom是添加自定义事件的,也可以触发它,它还以冒泡

火狐的一个官方说明 官方说明

一篇比较详细的介绍,还有例子 点点点

自定义事件是可以用chrome看到的,如图

手势的基本实现原理

tap,hold,swipe都是js没有的事件,都是由,touchstart,touchmove,touchend touchcancel这些事件组合而成的

实现原理就是通过绑定document的”touchstart touchmove touchend touchcancel“事件

当touch到元素,查看手势是否符合tap,swipe的原则

如果符合原则,就触发元素绑定的相关事件


判断移动了多少位置

比如我点击了元素a,我就就得记下点击时的位置,计算方式如下

touches[0].pageX,touches[0].pageY

这个是手指点击的位置离页面顶端的位置(或者是页面的左边)

然后再touchmove时记下相关的位置

在touchend或者touchcancel时,用2个数据,算一下移动了多少就行了

ps:touchend和touchcancel是没有event的所以必须在touchmove里面记录位置

阉割源码解析

touch.js 以我的水平来看,并不能很流畅的阅读源码...

而且有些手势也不是很常用,做了些阉割,写了些注释,方便理解

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>wo ca!~</title>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
</head>
<style>
    .xx{width: 200px;background: #ccc; height: 100px;}
</style>
<body>
<div id="vv" class="xx"></div>
<br>
<div id="ss" class="xx"></div>
<br>
<div id="ss1" class="xx">1</div>
<br>
<div id="ss2" class="xx">1</div>
<br>
<div id="ss3" class="xx"></div>
<br>
<div id="ss4" class="xx"></div>

<script>
    (function(){
        var utils = {};
        //获取元素的点击位置
        utils.getPosOfEvent = function(ev){
            var posi = [];
            var src = null;
            for (var t = 0, len = ev.touches.length; t < len; t++) {
                src = ev.touches[t];
                posi.push({
                    x: src.pageX,
                    y: src.pageY
                });
            }
            return posi;
        }
        utils.getType = function(obj) {
            return Object.prototype.toString.call(obj).match(/s([a-z|A-Z]+)/)[1].toLowerCase();
        };
        //获取点击的手指数量        
        utils.getFingers = function(ev) {
            return ev.touches ? ev.touches.length : 1;
        };
        utils.isTouchMove = function(ev) {
            return ev.type === 'touchmove';
        };
        //是否已经结束了手势
        utils.isTouchEnd = function(ev) {
            return (ev.type === 'touchend' || ev.type === 'touchcancel');
        };
        //算2点之间的距离        
        utils.getDistance = function(pos1, pos2) {
            var x = pos2.x - pos1.x,
                y = pos2.y - pos1.y;
            return Math.sqrt((x * x) + (y * y));
        };
        //算角度
        utils.getAngle = function(p1, p2) {
            return Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
        };
        //根据角度 返回up down left right
        utils.getDirectionFromAngle = function(agl) {
            var directions = {
                up: agl < -45 && agl > -135,
                down: agl >= 45 && agl < 135,
                left: agl >= 135 || agl <= -135,
                right: agl >= -45 && agl <= 45
            };
            for (var key in directions) {
                if (directions[key]) return key;
            }
            return null;
        };
        utils.reset = function() {
            startEvent = moveEvent = endEvent = null;
            __tapped = __touchStart = startSwiping = false;
            pos = {start: null,move: null,end: null};
        };

        //ua
        utils.env = (function() {
            var os = {}, ua = navigator.userAgent,
                android = ua.match(/(Android)[s/]+([d.]+)/),
                ios = ua.match(/(iPad|iPhone|iPod)s+OSs([d_.]+)/),
                wp = ua.match(/(Windowss+Phone)s([d.]+)/),
                isWebkit = /WebKit/[d.]+/i.test(ua),
                isSafari = ios ? (navigator.standalone ? isWebkit : (/Safari/i.test(ua) && !/CriOS/i.test(ua) && !/MQQBrowser/i.test(ua))) : false;
            if (android) {
                os.android = true;
                os.version = android[2];
            }
            if (ios) {
                os.ios = true;
                os.version = ios[2].replace(/_/g, '.');
                os.ios7 = /^7/.test(os.version);
                if (ios[1] === 'iPad') {
                    os.ipad = true;
                } else if (ios[1] === 'iPhone') {
                    os.iphone = true;
                    os.iphone5 = screen.height == 568;
                } else if (ios[1] === 'iPod') {
                    os.ipod = true;
                }
            }
            if (isWebkit) {
                os.webkit = true;
            }
            if (isSafari) {
                os.safari = true;
            }
            return os;
        })();        

        //已配置  tap hold swipe表示是否开启手势
        //tapTime tap事件延迟触发的时间
        //holdTime  hold事件多少秒后触发
        //tapMaxDistance 触发tap的时候 最小的移动范围
        //swipeMinDistance 触发swipe的时候 最小的移动范围
        //swipeTime  touchstart 到touchend之前的时间 如果小于swipeTime  才会触发swipe手势
        var config = {
            tap: true,
            tapMaxDistance: 10,
            hold: true,
            tapTime: 200,
            holdTime: 650,
            swipe: true,
            swipeTime: 300,
            swipeMinDistance: 18
        };
        var smrEventList = {
            TOUCH_START: 'touchstart',
            TOUCH_MOVE: 'touchmove',
            TOUCH_END: 'touchend',
            TOUCH_CANCEL: 'touchcancel',
            SWIPE_START: 'swipestart',
            SWIPING: 'swiping',
            SWIPE_END: 'swipeend',
            SWIPE_LEFT: 'swipeleft',
            SWIPE_RIGHT: 'swiperight',
            SWIPE_UP: 'swipeup',
            SWIPE_DOWN: 'swipedown',
            SWIPE: 'swipe',
            HOLD: 'hold',
            TAP: 'tap',
        };
        /** 手势识别 */
        //记录 开始 移动 结束时候的位置
        var pos = {
            start: null,
            move: null,
            end: null
        };
        var __touchStart = true;
        var __tapped;
        var __prev_tapped_end_time;
        var __prev_tapped_pos;
        var __holdTimer = null;
        var startTime;
        var startEvent;
        var moveEvent;
        var endEvent;
        var startSwiping;


        var gestures = {
            swipe: function(ev) {
                var el = ev.target;
                if (!__touchStart || !pos.move || utils.getFingers(ev) > 1) {
                    return;
                }
                   //计算 时间  距离  角度
                var now = Date.now();
                var touchTime = now - startTime;
                var distance = utils.getDistance(pos.start[0], pos.move[0]);
                var angle = utils.getAngle(pos.start[0], pos.move[0]);
                var direction = utils.getDirectionFromAngle(angle);
                var touchSecond = touchTime / 1000;
                var eventObj = {
                    type: smrEventList.SWIPE,
                    originEvent: ev,
                    direction: direction,
                    distance: distance,
                    distanceX: pos.move[0].x - pos.start[0].x,
                    distanceY: pos.move[0].y - pos.start[0].y,
                    x: pos.move[0].x - pos.start[0].x,
                    y: pos.move[0].y - pos.start[0].y,
                    angle: angle,
                    duration: touchTime,
                    fingersCount: utils.getFingers(ev)
                };
                if (config.swipe) {
                    var swipeTo = function() {
                        var elt = smrEventList;
                        switch (direction) {
                            case 'up':
                                engine.trigger(el, elt.SWIPE_UP, eventObj);
                                break;
                            case 'down':
                                engine.trigger(el, elt.SWIPE_DOWN, eventObj);
                                break;
                            case 'left':
                                engine.trigger(el, elt.SWIPE_LEFT, eventObj);
                                break;
                            case 'right':
                                engine.trigger(el, elt.SWIPE_RIGHT, eventObj);
                                break;
                        }
                    };
                    if (!startSwiping) {
                        eventObj.fingerStatus = eventObj.swipe = 'start';
                        //大于tap的最小距离 才算进入swipe手势
                        if(distance>config.tapMaxDistance){
                            startSwiping = true;
                        }                        
                    } else if (utils.isTouchMove(ev)) {
                        eventObj.fingerStatus = eventObj.swipe = 'move';
                        engine.trigger(el, smrEventList.SWIPING, eventObj);
                    } else if (utils.isTouchEnd(ev)) {
                        eventObj.fingerStatus = eventObj.swipe = 'end';
                        //事件要短  距离要有点远
                        if (config.swipeTime > touchTime && distance > config.swipeMinDistance) {
                            swipeTo();
                            engine.trigger(el, smrEventList.SWIPE, eventObj, false);
                        }
                    }
                }
            },
            tap : function(ev){
                var el = ev.target;
                //如果设置了tap为true  才会触发该手势
                if (config.tap) {
                    var now = Date.now();
                    var touchTime = now - startTime;
                    var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]);
                    clearTimeout(__holdTimer);
                    //如果移动的距离比设置的距离大(10)  就不算是tap    
                    if (config.tapMaxDistance < distance) return;

                    __tapped = true;
                    __prev_tapped_end_time = now;
                    __prev_tapped_pos = pos.start[0];
                    __tapTimer = setTimeout(function() {
                            engine.trigger(el, smrEventList.TAP, {
                                type: smrEventList.TAP,
                                originEvent: ev
                            });
                        },
                        config.tapTime);
                }
            },
            hold: function(ev) {
                var el = ev.target;
                //如果设置了hold为true  才会触发该手势
                if (config.hold) {
                    clearTimeout(__holdTimer);
                    __holdTimer = setTimeout(function() {
                            if (!pos.start) return;
                            var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]);
                            //如果移动的距离大于配置的距离(10)  就不触发hold
                            if (config.tapMaxDistance < distance) return;

                            if (!__tapped) {
                                engine.trigger(el, "hold", {
                                    type: 'hold',
                                    originEvent: ev,
                                    fingersCount: utils.getFingers(ev),
                                    position: pos.start[0]
                                });
                            }
                        },
                        config.holdTime);
                }
            }
        }

        /** 底层事件绑定/代理支持  */
        var engine = {
            proxyid: 0,
            proxies: [],
            trigger : function(el, evt, detail){
                detail = detail || {};
                var e, opt = {
                        bubbles: true,
                        cancelable: true,
                        detail: detail
                    };
                try {
                    //这里是触发 自定义事件
                    if (typeof CustomEvent !== 'undefined') {
                        e = new CustomEvent(evt, opt);
                        if (el) {
                            el.dispatchEvent(e);
                        }
                    } else {
                        e = document.createEvent("CustomEvent");
                        e.initCustomEvent(evt, true, true, detail);
                        if (el) {
                            el.dispatchEvent(e);
                        }
                    }
                } catch (ex) {
                    console.warn("Touch.js is not supported by environment.");
                }
            },
            bind: function(el, evt, handler) {
                el.listeners = el.listeners || {};
                //proxy才是真正元素绑定的事件
                var proxy = function(e) {
                    //对ios7的一个兼容  也不知道是什么原理

                    if (utils.env.ios7) {
                        utils.forceReflow();
                    }

                    e.originEvent = e;
                    for (var p in e.detail) {
                        if (p !== 'type') {
                            e[p] = e.detail[p];
                        }
                    }
                    var returnValue = handler.call(e.target, e);
                    if (typeof returnValue !== "undefined" && !returnValue) {
                        e.stopPropagation();
                        e.preventDefault();
                    }
                };

                if (!el.listeners[evt]) {
                    el.listeners[evt] = [proxy];
                } else {
                    el.listeners[evt].push(proxy);
                }

                handler.proxy = handler.proxy || {};
                if (!handler.proxy[evt]) {
                    handler.proxy[evt] = [this.proxyid++];
                } else {
                    handler.proxy[evt].push(this.proxyid++);
                }
                this.proxies.push(proxy);
                if (el.addEventListener) {
                    el.addEventListener(evt, proxy, false);
                }
            },
            unbind : function(el, evt){
                var handlers = el.listeners[evt];
                if (handlers && handlers.length) {
                    handlers.forEach(function(handler) {
                        el.removeEventListener(evt, handler, false);
                    });
                }
            }
        }

        var _on = function(el,evt,handler) {
            //绑定事件  支持多元素 多事件绑定噢
            var evts = evt.split(" ");
            var els = utils.getType(el) === 'string' ? document.querySelectorAll(el) : [el];
            
            evts.forEach(function(evt) {
                for(var i=0,len=els.length;i<len;i++){
                    engine.bind(els[i], evt, handler);
                }
            });            
        };

        var _off = function(els,evts,handler) {
            //删除绑定事件  支持多元素 多事件删除绑定噢
            var els = utils.getType(els) === 'string' ? document.querySelectorAll(els) : els;
            els = els.length ? Array.prototype.slice.call(els) : [els];
            els.forEach(function(el) {
                evts = evts.split(" ");
                evts.forEach(function(evt) {
                    engine.unbind(el, evt, handler);
                });
            });
            return;
        };        

        //这个函数很重要
        // doucment的触屏事件全部在这个里面
        var handlerOriginEvent = function(ev) {
            var el = ev.target;
            switch (ev.type) {
                case 'touchstart':
                    //记录下刚开始点击的事件和位置
                    __touchStart = true;
                    if (!pos.start || pos.start.length < 2) {
                        pos.start = utils.getPosOfEvent(ev);
                    }
                    startTime = Date.now();
                    startEvent = ev;
                    gestures.hold(ev);                
                    break;
                case 'touchmove':
                    if (!__touchStart || !pos.start) return;
                    //记录滑动过程中的位置
                    pos.move = utils.getPosOfEvent(ev);
                    gestures.swipe(ev);
                    break;
                case 'touchend':
                case 'touchcancel':
                    if (!__touchStart) return;
                    endEvent = ev;
                    //.......
                    if (startSwiping) {
                        gestures.swipe(ev);
                    } else {
                        gestures.tap(ev);
                    }                    

                    utils.reset();
                    if (ev.touches && ev.touches.length === 1) {
                        __touchStart = false;
                    }
                    break;                
            }
        }

        var init = function(){
            //给 document 绑定 下面这些事件
            var touchEvents = 'touchstart touchmove touchend touchcancel';
            touchEvents.split(" ").forEach(function(evt) {
                document.addEventListener(evt, handlerOriginEvent, false);
            });
        }
        init();
        window.touch = {
            on  : _on,
            off : _off
        };
    })();


    touch.on("#vv","tap",function(){
        ss1.innerHTML = ~~ss1.innerHTML+1;
    });

    touch.on("#vv","swipeleft",function(){
        ss2.innerHTML = ~~ss2.innerHTML+1;
    });    
    touch.on("#vv","swiperight",function(){
        ss2.innerHTML = ~~ss2.innerHTML-1;
    });
 
    touch.on("#vv","hold",function(){
        ss.innerHTML = ~~ss.innerHTML+1;
    });
</script>    
</body>
</html>

支持移动端 pc端的阉割源码解析

taobao的页面在ipad上,图片的轮询是支持手势滑动的,天涯的也一样,在pc就支持click了,所以手势封装也是支持pc和移动端才好

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>wo ca!~</title>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
</head>
<style>
    .xx{width: 200px;background: #ccc; height: 100px;}
</style>
<body>
<div id="vv" class="xx"></div>
<br>
<div id="ss" class="xx"></div>
<br>
<div id="ss1" class="xx">1</div>
<br>
<div id="ss2" class="xx">1</div>
<br>
<div id="ss3" class="xx"></div>
<br>
<div id="ss4" class="xx"></div>

<script>
    (function(){
        var utils = {};
        //获取元素的点击位置
        utils.getPosOfEvent = function(ev){
            if (this.hasTouch) {
                var posi = [];
                var src = null;

                for (var t = 0, len = ev.touches.length; t < len; t++) {
                    src = ev.touches[t];
                    posi.push({
                        x: src.pageX,
                        y: src.pageY
                    });
                }
                return posi;
            } else {
                return [{
                    x: ev.pageX,
                    y: ev.pageY
                }];
            }
        }
        utils.hasTouch = ('ontouchstart' in window);
        utils.PCevts = {
            'touchstart': 'mousedown',
            'touchmove': 'mousemove',
            'touchend': 'mouseup',
            'touchcancel': 'mouseout'
        };
        utils.getPCevts = function(evt) {
            return this.PCevts[evt] || evt;
        };        
        utils.getType = function(obj) {
            return Object.prototype.toString.call(obj).match(/s([a-z|A-Z]+)/)[1].toLowerCase();
        };
        //获取点击的手指数量        
        utils.getFingers = function(ev) {
            return ev.touches ? ev.touches.length : 1;
        };
        utils.isTouchMove = function(ev) {
            return (ev.type === 'touchmove' || ev.type === 'mousemove');
        };
        //是否已经结束了手势
        utils.isTouchEnd = function(ev) {
            return (ev.type === 'touchend' || ev.type === 'mouseup' || ev.type === 'touchcancel');
        };
        //算2点之间的距离        
        utils.getDistance = function(pos1, pos2) {
            var x = pos2.x - pos1.x,
                y = pos2.y - pos1.y;
            return Math.sqrt((x * x) + (y * y));
        };
        //算角度
        utils.getAngle = function(p1, p2) {
            return Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
        };
        //根据角度 返回up down left right
        utils.getDirectionFromAngle = function(agl) {
            var directions = {
                up: agl < -45 && agl > -135,
                down: agl >= 45 && agl < 135,
                left: agl >= 135 || agl <= -135,
                right: agl >= -45 && agl <= 45
            };
            for (var key in directions) {
                if (directions[key]) return key;
            }
            return null;
        };
        utils.reset = function() {
            startEvent = moveEvent = endEvent = null;
            __tapped = __touchStart = startSwiping = false;
            pos = {start: null,move: null,end: null};
        };

        //ua
        utils.env = (function() {
            var os = {}, ua = navigator.userAgent,
                android = ua.match(/(Android)[s/]+([d.]+)/),
                ios = ua.match(/(iPad|iPhone|iPod)s+OSs([d_.]+)/),
                wp = ua.match(/(Windowss+Phone)s([d.]+)/),
                isWebkit = /WebKit/[d.]+/i.test(ua),
                isSafari = ios ? (navigator.standalone ? isWebkit : (/Safari/i.test(ua) && !/CriOS/i.test(ua) && !/MQQBrowser/i.test(ua))) : false;
            if (android) {
                os.android = true;
                os.version = android[2];
            }
            if (ios) {
                os.ios = true;
                os.version = ios[2].replace(/_/g, '.');
                os.ios7 = /^7/.test(os.version);
                if (ios[1] === 'iPad') {
                    os.ipad = true;
                } else if (ios[1] === 'iPhone') {
                    os.iphone = true;
                    os.iphone5 = screen.height == 568;
                } else if (ios[1] === 'iPod') {
                    os.ipod = true;
                }
            }
            if (isWebkit) {
                os.webkit = true;
            }
            if (isSafari) {
                os.safari = true;
            }
            return os;
        })();        

        //已配置  tap hold swipe表示是否开启手势
        //tapTime tap事件延迟触发的时间
        //holdTime  hold事件多少秒后触发
        //tapMaxDistance 触发tap的时候 最小的移动范围
        //swipeMinDistance 触发swipe的时候 最小的移动范围
        //swipeTime  touchstart 到touchend之前的时间 如果小于swipeTime  才会触发swipe手势
        var config = {
            tap: true,
            tapMaxDistance: 10,
            hold: true,
            tapTime: 200,
            holdTime: 650,
            swipe: true,
            swipeTime: 300,
            swipeMinDistance: 18
        };
        var smrEventList = {
            TOUCH_START: 'touchstart',
            TOUCH_MOVE: 'touchmove',
            TOUCH_END: 'touchend',
            TOUCH_CANCEL: 'touchcancel',
            MOUSE_DOWN: 'mousedown',
            MOUSE_MOVE: 'mousemove',
            MOUSE_UP: 'mouseup',
            CLICK: 'click',
            PINCH_START: 'pinchstart',
            PINCH_END: 'pinchend',
            PINCH: 'pinch',
            PINCH_IN: 'pinchin',
            PINCH_OUT: 'pinchout',
            ROTATION_LEFT: 'rotateleft',
            ROTATION_RIGHT: 'rotateright',
            ROTATION: 'rotate',
            SWIPE_START: 'swipestart',
            SWIPING: 'swiping',
            SWIPE_END: 'swipeend',
            SWIPE_LEFT: 'swipeleft',
            SWIPE_RIGHT: 'swiperight',
            SWIPE_UP: 'swipeup',
            SWIPE_DOWN: 'swipedown',
            SWIPE: 'swipe',
            DRAG: 'drag',
            DRAGSTART: 'dragstart',
            DRAGEND: 'dragend',
            HOLD: 'hold',
            TAP: 'tap',
            DOUBLE_TAP: 'doubletap'
        };
        /** 手势识别 */
        //记录 开始 移动 结束时候的位置
        var pos = {
            start: null,
            move: null,
            end: null
        };
        var __touchStart = false;
        var __tapped;
        var __prev_tapped_end_time;
        var __prev_tapped_pos;
        var __holdTimer = null;
        var startTime=0;
        var startEvent;
        var moveEvent;
        var endEvent;
        var startSwiping;


        var gestures = {
            swipe: function(ev) {
                var el = ev.target;

                if (!__touchStart || !pos.move || utils.getFingers(ev) > 1) {
                    return;
                }

                   //计算 时间  距离  角度
                var now = Date.now();
                var touchTime = now - startTime;
                var distance = utils.getDistance(pos.start[0], pos.move[0]);
                var angle = utils.getAngle(pos.start[0], pos.move[0]);
                var direction = utils.getDirectionFromAngle(angle);
                var touchSecond = touchTime / 1000;
                var eventObj = {
                    type: smrEventList.SWIPE,
                    originEvent: ev,
                    direction: direction,
                    distance: distance,
                    distanceX: pos.move[0].x - pos.start[0].x,
                    distanceY: pos.move[0].y - pos.start[0].y,
                    x: pos.move[0].x - pos.start[0].x,
                    y: pos.move[0].y - pos.start[0].y,
                    angle: angle,
                    duration: touchTime,
                    fingersCount: utils.getFingers(ev)
                };
                if (config.swipe) {
                    var swipeTo = function() {
                        var elt = smrEventList;
                        switch (direction) {
                            case 'up':
                                engine.trigger(el, elt.SWIPE_UP, eventObj);
                                break;
                            case 'down':
                                engine.trigger(el, elt.SWIPE_DOWN, eventObj);
                                break;
                            case 'left':
                                engine.trigger(el, elt.SWIPE_LEFT, eventObj);
                                break;
                            case 'right':
                                engine.trigger(el, elt.SWIPE_RIGHT, eventObj);
                                break;
                        }
                    };
                    if (!startSwiping) {
                        eventObj.fingerStatus = eventObj.swipe = 'start';
                        //大于tap的最小距离 才算进入swipe手势
                        if(distance>config.tapMaxDistance){
                            startSwiping = true;
                        }                        
                    } else if (utils.isTouchMove(ev)) {
                        eventObj.fingerStatus = eventObj.swipe = 'move';
                        engine.trigger(el, smrEventList.SWIPING, eventObj);
                    } else if (utils.isTouchEnd(ev)|| ev.type === 'mouseout') {
                        

                        eventObj.fingerStatus = eventObj.swipe = 'end';
                        //事件要短  距离要有点远
                        if (config.swipeTime > touchTime && distance > config.swipeMinDistance) {

                            swipeTo();
                            engine.trigger(el, smrEventList.SWIPE, eventObj, false);
                        }
                    }
                }
            },
            tap : function(ev){
                var el = ev.target;
                //如果设置了tap为true  才会触发该手势
                if (config.tap) {
                    var now = Date.now();
                    var touchTime = now - startTime;
                    var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]);
                    clearTimeout(__holdTimer);
                    //如果移动的距离比设置的距离大(10)  就不算是tap    
                    if (config.tapMaxDistance < distance) return;

                    __tapped = true;
                    __prev_tapped_end_time = now;
                    __prev_tapped_pos = pos.start[0];
                    __tapTimer = setTimeout(function() {
                            engine.trigger(el, smrEventList.TAP, {
                                type: smrEventList.TAP,
                                originEvent: ev
                            });
                        },
                        config.tapTime);
                }
            },
            hold: function(ev) {
                var el = ev.target;
                //如果设置了hold为true  才会触发该手势
                if (config.hold) {
                    clearTimeout(__holdTimer);
                    __holdTimer = setTimeout(function() {
                            if (!pos.start) return;
                            var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]);
                            //如果移动的距离大于配置的距离(10)  就不触发hold
                            if (config.tapMaxDistance < distance) return;

                            if (!__tapped) {
                                engine.trigger(el, "hold", {
                                    type: 'hold',
                                    originEvent: ev,
                                    fingersCount: utils.getFingers(ev),
                                    position: pos.start[0]
                                });
                            }
                        },
                        config.holdTime);
                }
            }
        }

        /** 底层事件绑定/代理支持  */
        var engine = {
            proxyid: 0,
            proxies: [],
            trigger : function(el, evt, detail){
                detail = detail || {};
                var e, opt = {
                        bubbles: true,
                        cancelable: true,
                        detail: detail
                    };
                try {
                    //这里是触发 自定义事件
                    if (typeof CustomEvent !== 'undefined') {
                        e = new CustomEvent(evt, opt);
                        if (el) {
                            el.dispatchEvent(e);
                        }
                    } else {
                        e = document.createEvent("CustomEvent");
                        e.initCustomEvent(evt, true, true, detail);
                        if (el) {
                            el.dispatchEvent(e);
                        }
                    }
                } catch (ex) {
                    console.warn("Touch.js is not supported by environment.");
                }
            },
            bind: function(el, evt, handler) {
                el.listeners = el.listeners || {};
                //proxy才是真正元素绑定的事件
                var proxy = function(e) {
                    //对ios7的一个兼容  也不知道是什么原理

                    if (utils.env.ios7) {
                        utils.forceReflow();
                    }

                    e.originEvent = e;
                    for (var p in e.detail) {
                        if (p !== 'type') {
                            e[p] = e.detail[p];
                        }
                    }
                    var returnValue = handler.call(e.target, e);
                    if (typeof returnValue !== "undefined" && !returnValue) {
                        e.stopPropagation();
                        e.preventDefault();
                    }
                };

                if (!el.listeners[evt]) {
                    el.listeners[evt] = [proxy];
                } else {
                    el.listeners[evt].push(proxy);
                }

                handler.proxy = handler.proxy || {};
                if (!handler.proxy[evt]) {
                    handler.proxy[evt] = [this.proxyid++];
                } else {
                    handler.proxy[evt].push(this.proxyid++);
                }
                this.proxies.push(proxy);
                if (el.addEventListener) {
                    el.addEventListener(evt, proxy, false);
                }
            },
            unbind : function(el, evt){
                var handlers = el.listeners[evt];
                if (handlers && handlers.length) {
                    handlers.forEach(function(handler) {
                        el.removeEventListener(evt, handler, false);
                    });
                }
            }
        }

        var _on = function(el,evt,handler) {
            //绑定事件  支持多元素 多事件绑定噢
            var evts = evt.split(" ");
            var els = utils.getType(el) === 'string' ? document.querySelectorAll(el) : [el];
            
            evts.forEach(function(evt) {
                if (!utils.hasTouch) {
                    evt = utils.getPCevts(evt);
                }                
                for(var i=0,len=els.length;i<len;i++){
                    engine.bind(els[i], evt, handler);
                }
            });            
        };

        var _off = function(els,evts,handler) {
            //删除绑定事件  支持多元素 多事件删除绑定噢
            var els = utils.getType(els) === 'string' ? document.querySelectorAll(els) : els;
            els = els.length ? Array.prototype.slice.call(els) : [els];
            els.forEach(function(el) {
                evts = evts.split(" ");
                evts.forEach(function(evt) {
                    if (!utils.hasTouch) {
                        evt = utils.getPCevts(evt);
                    }                    
                    engine.unbind(el, evt, handler);
                });
            });
            return;
        };        

        //这个函数很重要
        // doucment的触屏事件全部在这个里面
        var handlerOriginEvent = function(ev) {
            var el = ev.target;

            switch (ev.type) {
                case 'mousedown':
                case 'touchstart':
                    //记录下刚开始点击的事件和位置
                    __touchStart = true;
                    if (!pos.start || pos.start.length < 2) {
                        pos.start = utils.getPosOfEvent(ev);
                    }
                    startTime = Date.now();
                    startEvent = ev;
                    gestures.hold(ev);                
                    break;
                case 'touchmove':
                case 'mousemove':
                    if (!__touchStart || !pos.start) return;
                    //记录滑动过程中的位置
                    pos.move = utils.getPosOfEvent(ev);
                    gestures.swipe(ev);
                    break;
                case 'touchend':
                case 'touchcancel':
                case 'mouseup':
                case 'moudeout':                
                    if (!__touchStart) return;
                    endEvent = ev;
                    //.......
                    if (startSwiping) {
                        gestures.swipe(ev);
                    } else {
                        gestures.tap(ev);
                    }                    

                    utils.reset();
                    if (ev.touches && ev.touches.length === 1) {
                        __touchStart = false;
                    }
                    break;                
            }
        }

        var init = function(){
            //给 document 绑定 下面这些事件
            var mouseEvents = 'mouseup mousedown mousemove',
                touchEvents = 'touchstart touchmove touchend touchcancel';
            var bindingEvents = utils.hasTouch ? touchEvents : mouseEvents;

            bindingEvents.split(" ").forEach(function(evt) {
                document.addEventListener(evt, handlerOriginEvent, false);
            });
        }
        init();
        window.touch = {
            on  : _on,
            off : _off
        };
    })();

    touch.on("#vv","tap",function(){
        ss1.innerHTML = ~~ss1.innerHTML+1;
    });

    touch.on("#vv","swipeleft",function(){
        ss2.innerHTML = ~~ss2.innerHTML+1;
    });    
    touch.on("#vv","swiperight",function(){
        ss2.innerHTML = ~~ss2.innerHTML-1;
    });
 
    touch.on("#vv","hold",function(){
        ss.innerHTML = ~~ss.innerHTML+1;
    });
</script>    
</body>
</html>

zepto的手势源码解析

zepto自己以移动端的jq自喻,然后提供了一套移动端的手势

touch的下载地址 https://github.com/madrobby/zepto/blob/master/src/touch.js#files

这个touch的好处就是可以支持jq的事件绑定方式,比如$("#xx").bind("tap",fun),容易理解,容易上手

这个touch的实现方式和百度的touch实现基本是一样的,document去绑定touchstart,touchmove,touchend,touchcancel然后经过一些列的判断

去掉ms的兼容 , 去掉一些手势后的代码    全部代码下载地址    https://github.com/madrobby/zepto/blob/master/src/touch.js#files

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>wo ca!~</title>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
</head>
<style>
    .xx{width: 200px;background: #ccc; height: 100px;}
</style>
<body>
<div id="vv" class="xx a"></div>
<br>
<div id="ss" class="xx a">1</div>
<br>
<div id="ss1" class="xx">1</div>
<br>
<div id="ss2" class="xx">1</div>
<br>


<script src="http://static.paipaiimg.com/paipai_h5/js/ttj/zepto.min.js"></script>
<script >
//     Zepto.js
//     (c) 2010-2015 Thomas Fuchs
//     Zepto.js may be freely distributed under the MIT license.

;(function($){
  var touch = {},
    touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
    longTapDelay = 750,
    gesture

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

  function longTap() {
    longTapTimeout = null
    if (touch.last) {
      touch.el.trigger('longTap')
      touch = {}
    }
  }

  function cancelLongTap() {
    if (longTapTimeout) clearTimeout(longTapTimeout)
    longTapTimeout = null
  }

  function cancelAll() {
    if (touchTimeout) clearTimeout(touchTimeout)
    if (tapTimeout) clearTimeout(tapTimeout)
    if (swipeTimeout) clearTimeout(swipeTimeout)
    if (longTapTimeout) clearTimeout(longTapTimeout)
    touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null
    touch = {}
  }

  function isPrimaryTouch(event){
    return (event.pointerType == 'touch' ||
      event.pointerType == event.MSPOINTER_TYPE_TOUCH)
      && event.isPrimary
  }

  $(document).ready(function(){
    var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType

    $(document)
      .on('touchstart', function(e){
        //取第一个手指的信息
        firstTouch = e.touches[0]
        if (e.touches && e.touches.length === 1 && touch.x2) {
          // Clear out touch movement data if we have it sticking around
          // This can occur if touchcancel doesn't fire due to preventDefault, etc.
          touch.x2 = undefined
          touch.y2 = undefined
        }
        //记录点下的时候
        now = Date.now()
        delta = now - (touch.last || now)
        touch.el = $('tagName' in firstTouch.target ?
          firstTouch.target : firstTouch.target.parentNode)
        touchTimeout && clearTimeout(touchTimeout)
        //记录点下的位置
        touch.x1 = firstTouch.pageX
        touch.y1 = firstTouch.pageY
        touch.last = now
        longTapTimeout = setTimeout(longTap, longTapDelay)
      })
      .on('touchmove', function(e){
        firstTouch = e.touches[0];
        //记录移动到的位置 和移动的距离
        cancelLongTap();
        touch.x2 = firstTouch.pageX
        touch.y2 = firstTouch.pageY

        deltaX += Math.abs(touch.x1 - touch.x2)
        deltaY += Math.abs(touch.y1 - touch.y2)
      })
      .on('touchend', function(e){
        cancelLongTap()
        //判断移动的范围来判断是 tap 还是 swipe
        // swipe
        if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
            (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))

          swipeTimeout = setTimeout(function() {
            touch.el.trigger('swipe')
            touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
            touch = {}
          }, 0)

        // normal tap
        else if ('last' in touch)
          // don't fire tap when delta position changed by more than 30 pixels,
          // for instance when moving to a point and back to origin
          if (deltaX < 30 && deltaY < 30) {
            // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
            // ('tap' fires before 'scroll')
            tapTimeout = setTimeout(function() {
              // trigger universal 'tap' with the option to cancelTouch()
              // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
              var event = $.Event('tap')
              event.cancelTouch = cancelAll
              touch.el.trigger(event)
            }, 0)
          } else {
            touch = {}
          }
          deltaX = deltaY = 0

      })
      // when the browser window loses focus,
      // for example when a modal dialog is shown,
      // cancel all ongoing events
      .on('touchcancel', cancelAll)

    // scrolling the window indicates intention of the user
    // to scroll, not tap or swipe, so cancel all ongoing events
    $(window).on('scroll', cancelAll)
  })

  ;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
    'tap',  'longTap'].forEach(function(eventName){
    $.fn[eventName] = function(callback){
      //给元素绑定上面的事件  
    return this.on(eventName, callback) 
    }
  })
})(Zepto);    
</script>
<script>
    $("#vv").bind("tap",function(){
        ss.innerHTML = ~~ss.innerHTML+1; 
    });
    $("#vv").bind("swipeLeft",function(){
        ss1.innerHTML = ~~ss1.innerHTML+1;
    });    
    $("#vv").bind("longTap",function(){
        ss2.innerHTML = ~~ss2.innerHTML+1;
    });        
</script>
</body>
</html>

一些我遇到的手势问题

问题1

在有些android的版本上 touchend不触发

在Android 4.0.x的版本上我遇到过,很蛋疼,比如小米1的最开始的版本就遇到过

如果在touchmove中加上 阻止默认行为 是可以的(e.preventDefault();),但是会带来另为一个严重的问题,就是无法向下滑动,真实无解的问题,好在这个版本已经离我们远去

这个问题的一些讨论

https://code.google.com/p/android/issues/detail?id=19827

http://stackoverflow.com/questions/7691551/touchend-event-in-ios-webkit-not-firing


问题2

透传的问题

透传应该分2中,

一种是上面的div隐藏,触发到下面的元素的click,都用tap就可解决,不要一个tap一个click 这样不好

另外一种是上层的元素隐藏,触发到下面input的聚焦,弹出键盘(最常见的场景,就是弹出个这招层,点关闭遮罩层的时候,下面有一个input)

这个问题都找不到好的解决方案,我在项目中的做法是有一个透明的遮罩层,先关闭遮罩层,在等280ms关闭这个透明的遮罩层

原文地址:https://www.cnblogs.com/wtcsy/p/touch.html