Zepto.js touch模块深入分析

目的:记录 Zepto.js touch模块 源码阅读

源码

//     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

        }



        function isPointerEventType(e, type) {

            return (e.type == 'pointer' + type ||

                e.type.toLowerCase() == 'mspointer' + type)

        }



    $(document).ready(function() {

        var now, delta, deltaX = 0,

            deltaY = 0,

            firstTouch, _isPointerType



        if ('MSGesture' in window) {

            gesture = new MSGesture()

            gesture.target = document.body

        }



        $(document)

            .bind('MSGestureEnd', function(e) {

                var swipeDirectionFromVelocity =

                    e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null;

                if (swipeDirectionFromVelocity) {

                    touch.el.trigger('swipe')

                    touch.el.trigger('swipe' + swipeDirectionFromVelocity)

                }

            })

            .on('touchstart MSPointerDown pointerdown', function(e) {

                if ((_isPointerType = isPointerEventType(e, 'down')) && !isPrimaryTouch(e)) return

                firstTouch = _isPointerType ? e : 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

                if (delta > 0 && delta <= 250) touch.isDoubleTap = true

                touch.last = now

                longTapTimeout = setTimeout(longTap, longTapDelay)

                // adds the current touch contact for IE gesture recognition

                if (gesture && _isPointerType) gesture.addPointer(e.pointerId);

            })

            .on('touchmove MSPointerMove pointermove', function(e) {

                if ((_isPointerType = isPointerEventType(e, 'move')) && !isPrimaryTouch(e)) return

                firstTouch = _isPointerType ? e : 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 MSPointerUp pointerup', function(e) {

                if ((_isPointerType = isPointerEventType(e, 'up')) && !isPrimaryTouch(e)) return

                cancelLongTap()



                // 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)



                            // trigger double tap immediately

                            if (touch.isDoubleTap) {

                                if (touch.el) touch.el.trigger('doubleTap')

                                touch = {}

                            }



                            // trigger single tap after 250ms of inactivity

                            else {

                                touchTimeout = setTimeout(function() {

                                    touchTimeout = null

                                    if (touch.el) touch.el.trigger('singleTap')

                                    touch = {}

                                }, 250)

                            }

                        }, 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 MSPointerCancel pointercancel', 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',

        'doubleTap', 'tap', 'singleTap', 'longTap'

    ].forEach(function(eventName) {

        $.fn[eventName] = function(callback) {

            return this.on(eventName, callback)

        }

    })

})(Zepto)
View Code

分析

var now, delta, touch = {};

$(document)

    .on('touchstart', startListener)

    .on('touchmove', moveListener)

    .on('touchend', endListener);

1、是单击还是双击

function startListener(e){

    now = Date.now();

    delta = now - (touch.last || now);



    // 手指连续轻触两次,时间间隔大于0,小于等于.25s,则为双击,反之单击

    if ( delta > 0 && delta <= 250 ) {

        touch.isDoubleTap = true;

    }

    touch.last = now;

}

2、处理手指长按

var longTapTimeout, longTapDelay = 750;

function longTap() {

    longTapTimeout = null

    if (touch.last) {

        touch.el.trigger('longTap')

        touch = {}

    }

}

function cancelLongTap() {

    if (longTapTimeout) clearTimeout(longTapTimeout)

    longTapTimeout = null

}



function startListener(e){

    // 默认就是长按,如果手指未移动和离开,超过.75s就触发longTap

    longTapTimeout = setTimeout(longTap, longTapDelay)

}

function moveListener(e){

    // 如果手指轻触屏幕后未超过.75s,则取消手指长按监听

    longTapTimeout = setTimeout(longTap, longTapDelay)

}

function endListener(e){

    // 如果手指轻触屏幕后未超过.75s,则取消手指长按监听

    longTapTimeout = setTimeout(longTap, longTapDelay)

}

3、是滑动(swipe)还是轻触(tap)

// 如果手指移动屏幕超过30像素,则触发相应的滑动事件,swipeLeft, swipeRight, swipeUp, swipeDown

function endListener(e){

    // 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);

    }

    else {

        // handle tap

        // 关于处理tap事件,请看第四点

    }

}

4、轻触 tap, singleTap, doubleTap

4.1、何时触发 tap ?

条件1:手指移动不超过30像素

if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||

   (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) {

    // swipe

}

else {

    // tap

}

条件2:依据条件1,基本上可以触发tap了,但是还考虑了另一种情况,手指滑动屏幕后又滑动到起始点,那么:

!Math.abs(touch.x1 - touch.x2) > 30) === !Math.abs(touch.y1 - touch.y2) > 30) === true;

为了不触发tap事件,这里又加了条件限制,理解这点很重要

if (deltaX < 30 && deltaY < 30) {

    // handle tap

}

注意:

deltaX !== Math.abs(touch.x1 - touch.x2);

deltaY !== Math.abs(touch.y1 - touch.y2); 

请看 moveListener 中的代码:

function moveListener(e){

    // ...

    touch.x2 = firstTouch.pageX

    touch.y2 = firstTouch.pageY



    deltaX += Math.abs(touch.x1 - touch.x2)

    deltaY += Math.abs(touch.y1 - touch.y2)

}

例: deltaX的计算,你懂得...

if ( Math.abs(touch.x1 - touch.x2) === 10 ) {

    deltaX = 10 + 9 + 8 + ... + 0;

}

 

4.2、处理tap,doubleTap,singleTap三者之间的关系

function cancelAll() {

    if (touchTimeout) clearTimeout(touchTimeout)

    if (tapTimeout) clearTimeout(tapTimeout)

    if (swipeTimeout) clearTimeout(swipeTimeout)

    if (longTapTimeout) clearTimeout(longTapTimeout)

    touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null

    // 这句很重要,将影响所有需要对touch对象属性判断的语句

    touch = {}

}

function endListener(e){

    tapTimeout = setTimeout(function() {

        var event = $.Event('tap')

        // tap事件对象event可以取消后续绑定的doubleTap, singleTap处理器

        event.cancelTouch = cancelAll

        touch.el.trigger(event)

        // 立即触发双击事件

        if (touch.isDoubleTap) {

            if (touch.el) touch.el.trigger('doubleTap')

            touch = {}

        }

        // 定时.25s后再触发单击事件

        else {

            touchTimeout = setTimeout(function() {

                touchTimeout = null

                if (touch.el) touch.el.trigger('singleTap')

                touch = {}

            }, 250)

        }

    }, 0)

}

例如:如何在tap事件处理器中取消 doubleTap或singleTap事件监听器

$('body')

    .on('tap', function(e){

        console.log('tap');

        // 执行下面语句将影响是否触发绑定的 doubleTap或singleTap 处理器

        e.cancelTouch();

    })

    .on('doubleTap', function(e){

        console.log('doubleTap');

    })

    .on('singleTap', function(e){

        console.log('singleTap');

    });



// 'tap'

5、兼容指针事件系统

// 判断是否是指针事件类型

function isPointerEventType(e, type) {

    return (e.type == 'pointer' + type ||

        e.type.toLowerCase() == 'mspointer' + type)

}



// 判断是否是第一个touch或pointer事件对象

function isPrimaryTouch(event) {

    return (event.pointerType == 'touch' ||

        event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary

}

// 如果是指针类型是 pointerdown 或 pointermove 或 pointerup 且 不是第一个touch 或 pointer 事件对象,返回空,

// 直接屏蔽了第二个、第三...的触摸处理

if ((_isPointerType = isPointerEventType(e, 'down')) && !isPrimaryTouch(e)) return

if ((_isPointerType = isPointerEventType(e, 'move')) && !isPrimaryTouch(e)) return

if ((_isPointerType = isPointerEventType(e, 'up')) && !isPrimaryTouch(e)) return

6、快捷注册事件

['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',

    'doubleTap', 'tap', 'singleTap', 'longTap'

].forEach(function(eventName) {

    $.fn[eventName] = function(callback) {

        return this.on(eventName, callback)

    }

});

你可以用 on 方法注册事件,也可以快捷注册,下面两种方式都是一样的,类似jQuery用法

$('body').on('tap', function(){ console.log('body trigger tap event'); });

$('body').tap(function(){ console.log('body trigger tap event'); });

篇尾总结

源码中大部分代码都已经解析完毕,如有不合理的地方,还请赐教,touch模块的中所有的事件都支持冒泡,但是不会对原生的touch事件产生影响,另外所有元素绑定的事件都是在文档document元素的touchend处理中触发,

如果页面中有一元素在原生touch事件中阻止了冒泡,那么页面中所有元素注册的 zepto touch事件都不会被触发,慎重慎重...,至此完毕,感谢阅读!!

 

你可能感兴趣的:(touch)