Skip to main content

Zepto's Touch Module Source Code Analysis

Free2015-09-04#JS#zepto的touch模块#zepto touch#zepto点透bug

How is the tap event implemented? This article explains its implementation principle in detail

Preface

Reading Zepto's touch module source code is to understand the specific implementation of tap, and then write the touch module for my own utility library my.js

(my.js is a mobile page utility library tailored according to business needs, does not support win phone)

I. Zepto and Tap-Through/Click-Through Problems

Zepto previously had click-through problems, fixed the in-page click-through problem in 2014 (or was it 12/13?), but currently (2015-8-22) still exists cross-page click-through problem

P.S. For detailed information about click-through problems, please see AnYuQingYang: Mobile Page Click-Through Problem Solution

II. Source Code Analysis

Zepto's touch module source code is as follows, with detailed comments (IE-related parts omitted):

//     Zepto.js
//     (c) 2010-2015 Thomas Fuchs
//     Zepto.js may be freely distributed under the MIT license.

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

    // Judge swipe direction, return Left, Right, Up, Down
    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')
    }

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

    // Cancel long press
    function cancelLongTap() {
        if (longTapTimeout) clearTimeout(longTapTimeout)
        longTapTimeout = null
    }

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

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

    // (ie) mouse event?
    function isPointerEventType(e, type) {
        return (e.type == 'pointer' + type ||
            e.type.toLowerCase() == 'mspointer' + type)
    }

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

        // IE gesture
        if ('MSGesture' in window) {
            gesture = new MSGesture()
            gesture.target = document.body
        }

        $(document)
            // Handle IE gesture end
            .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)
                }
            })



        // Handle finger touch
        .on('touchstart MSPointerDown pointerdown', function(e) {
            // Exclude non-touch devices
            if ((_isPointerType = isPointerEventType(e, 'down')) &&
                !isPrimaryTouch(e)) return
            // Get starting position data
            firstTouch = _isPointerType ? e : e.touches[0]
            // Reset end coordinates
            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
            }
            // Judge user action type
            now = Date.now()
            delta = now - (touch.last || now)   // Time difference from last touch
            touch.el = $('tagName' in firstTouch.target ?   // Element finger touched
                firstTouch.target : firstTouch.target.parentNode)
            touchTimeout && clearTimeout(touchTimeout)  // Reset touch event handler's Timeout ID
            // Record starting coordinates
            touch.x1 = firstTouch.pageX
            touch.y1 = firstTouch.pageY
            // Judge double tap
            if (delta > 0 && delta <= 250) touch.isDoubleTap = true
            touch.last = now    // Update last touch time
            // Register long press event handler ID (triggers long press after 750ms, will definitely judge cancel when finger leaves)
            longTapTimeout = setTimeout(longTap, longTapDelay)
            // Support IE gesture recognition
                // adds the current touch contact for IE gesture recognition
            if (gesture && _isPointerType) gesture.addPointer(e.pointerId);
        })


        // Handle finger swipe
        .on('touchmove MSPointerMove pointermove', function(e) {
            // Exclude non-touch devices
            if ((_isPointerType = isPointerEventType(e, 'move')) &&
                !isPrimaryTouch(e)) return
            // Read starting coordinates
            firstTouch = _isPointerType ? e : e.touches[0]
            // Cancel long press event handler
            cancelLongTap()
            // Record current point coordinates
            touch.x2 = firstTouch.pageX
            touch.y2 = firstTouch.pageY
            // Accumulate swiped distance
            deltaX += Math.abs(touch.x1 - touch.x2)
            deltaY += Math.abs(touch.y1 - touch.y2)
        })


        // Handle finger leave
        .on('touchend MSPointerUp pointerup', function(e) {
            // Exclude non-touch devices
            if ((_isPointerType = isPointerEventType(e, 'up')) &&
                !isPrimaryTouch(e)) return
            // Cancel long press event handler
            cancelLongTap()

            // swipe
            // Judge swipe action (horizontal or vertical distance from start to end exceeds 30px)
            if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
                (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
                // Register long press event handler ID (immediately prepare to execute long press)
                swipeTimeout = setTimeout(function() {
                touch.el.trigger('swipe')   // Trigger long press
                // Trigger up|down|left|right long press
                touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
                // Clear data, this touch ends
                touch = {}
            }, 0)

            // normal tap
            // Normal light touch
            else if ('last' in touch)   // If last contact time is recorded
            // 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 from touch to lift, horizontal and vertical distance swiped in between doesn't exceed 30px — is normal light touch
                if (deltaX < 30 && deltaY < 30) {
                    // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
                    // ('tap' fires before 'scroll')
                    // Immediately prepare to execute light touch, not execute immediately is to be able to cancel executing light touch when scroll
                    tapTimeout = setTimeout(function() {
                        // trigger universal 'tap' with the option to cancelTouch()
                        // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
                        // Trigger global tap, cancelTouch() can cancel singleTap, doubleTap events, for faster light touch response
                        var event = $.Event('tap')
                        event.cancelTouch = cancelAll
                        touch.el.trigger(event)

                        // trigger double tap immediately
                        // Immediately trigger doubleTap
                        if (touch.isDoubleTap) {
                            if (touch.el) touch.el.trigger('doubleTap')
                            touch = {}
                        }

                        // trigger single tap after 250ms of inactivity
                        // Trigger singleTap after 250ms
                        else {
                            touchTimeout = setTimeout(function() {
                                touchTimeout = null
                                if (touch.el) touch.el.trigger('singleTap')
                                touch = {}
                            }, 250)
                        }
                    }, 0)
                } else {
                    // If swiped in a circle and back to starting point, discard event data, no processing
                    touch = {}
                }
                // Reset horizontal, vertical swipe distance
                deltaX = deltaY = 0
            })



            // when the browser window loses focus,
            // for example when a modal dialog is shown,
            // cancel all ongoing events
            // When browser window loses focus, cancel all event handling actions
            .on('touchcancel MSPointerCancel pointercancel', cancelAll)




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


    // Register custom events
    ;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
        'doubleTap', 'tap', 'singleTap', 'longTap'
    ].forEach(function(eventName) {
        $.fn[eventName] = function(callback) {
            return this.on(eventName, callback)
        }
    })
})(Zepto)

III. Implementation Principle of tap Event

tap event is simulated through touch events, tap means "light touch", much faster than click (about 300ms). Specific implementation details won't be elaborated (comments in source code above are very clear), here say something off-topic

Implementing tap alone is very easy, dozens of lines of code is enough, but to ensure remaining functions work normally requires manually implementing a complete set of action events:

  • tap

  • singleTap

  • doubleTap

  • longTap

  • swipe

  • swipeLeft

  • swipeRight

  • swipeUp

  • swipeDown

If simply implementing tap, then swipe, double tap, long press and various other actions will all fail, Zepto's touch module only implements tap and swipe related actions, doesn't support complex gestures, if need to support complex gestures, recommend selecting hammer.js, hammer provides a complete set of gesture support (Note: hammer also has click-through problem, still need to manually handle this problem)

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment