preface

Mobile terminal native support touchStart, TouchMove, TouchEnd and other events, but in ordinary business we often need to use events such as Swipe, TAP, doubleTap, longTap to achieve the desired effect, for this kind of custom events how do they realize? Let’s analyze how this works from the touch module of Zepto.js. You can also view the touch.js source comments directly

Source warehouse

The original link

Event description

Swipe, swipeLeft, swipeRight, swipeUp, swipeDown,doubleTap, Tap, singleTap, longTap

The name of the event Event description
swipe Sliding event
swipeLeft ← Left slip event
swipeRight → Right slide event
swipeUp Up slip event
swipeDown ↓ Sliding event
doubleTap Double-click the event
tap Click events (non-native Click events)
singleTap Click event
longTap Long press event
; ['swipe'.'swipeLeft'.'swipeRight'.'swipeUp'.'swipeDown'.'doubleTap'.'tap'.'singleTap'.'longTap'].forEach(function(eventName){
  $.fn[eventName] = function(callback){ return this.on(eventName, callback) }
})

Copy the code

You can see Zepto has all of these methods built into the prototype, which means you can just use shorthand for example$('body').tap(callback)

precondition

Before we start looking at how these events are implemented, let’s look at some of the prerequisites

  • Partial internal variables
var touch = {},
    touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
    // Hold down the event timer time
    longTapDelay = 750,
    gesture

Copy the code

Touch: Used to store information about finger operations, such as the position of the finger when pressed, the coordinates when it left, etc.

TouchTimeout, tapTimeout swipeTimeout, longTapTimeout are stored singleTap, tap and swipe, longTap event timer.

LongTapDelay: Indicates the delay of the longTap timer

Gesture: Stores the ieGesture event object

  • Judgment of sliding direction (swipeDirection)

Let’s use the figure below and the corresponding code to understand how orientation is determined when sliding. It is important to note that the “coordinate system” in the browser is not quite the same as the coordinate system in mathematics, and the Y-axis has a little bit of the opposite meaning.

/** * Left, Right, Up, A * @param {} in Down * @param {} x1 * @param {} x2 * @param {} y1 * @param {} y2 */

function swipeDirection(x1, x2, y1, y2) {
  /** * 1. If the X-axis slides more than the Y-axis, then it slides left/right, otherwise it slides up/down * 2. If you're sliding left or right, and your starting point is bigger than your ending point, then you're sliding left * 3. If you're sliding up or down, and your starting point is bigger than your ending point, then you're sliding up * and the important thing to note is that the coordinates here are a little bit different from the math and the y-coordinate is a little bit the other way around * p1(1, 0) and p2(1, 1) */
  return Math.abs(x1 - x2) >=
    Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')}Copy the code
  • Trigger the long press event
function longTap() {
  longTapTimeout = null
  if (touch.last) {
    // Trigger the longTap event for the el element
    touch.el.trigger('longTap')
    touch = {}
  }
}

Copy the code

Cancel the longTapTimeout timer before triggering the long press event. Trigger the longTapTimeout timer if touch.last still exists. Why check touch.last? Long press events should not occur naturally.

  • Cancel the long press, and cancel all events
// Cancel the long press
function cancelLongTap() {
  if (longTapTimeout) clearTimeout(longTapTimeout)
  longTapTimeout = null
}

// Cancel all events

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

The method is similar: call clearTimeout to cancel the timer, then release the corresponding variable and wait for garbage collection.

Overall structure analysis



$(document).ready(function(){
  /** * now Current touch time * Delta time difference between two touches * deltaX x change * deltaY Y change * firstTouch information about the touch point * _isPointerType is pointerType */
  var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType

  $(document)
    .bind('MSGestureEnd'.function(e){
      // XXX will not look here
    })
    .on('touchstart MSPointerDown pointerdown'.function(e){
      // XXX look here
    })
    .on('touchmove MSPointerMove pointermove'.function(e){
      // XXX look here
    })
    .on('touchend MSPointerUp pointerup'.function(e){
      // XXX look here
    })
    .on('touchcancel MSPointerCancel pointercancel', cancelAll)

    $(window).on('scroll', cancelAll)
  })


Copy the code

The detailed code has been omitted to leave out the overall framework, and you can see that Zepto is in the DOM, When you’re ready, you add MSGestureEnd, TouchStart MSPointerDown Pointerdown, TouchMove MSPointerMove Pointermove, TouchCancel to the document MSPointerCancel, pointercancel, etc., and finally, scroll event on the window. We will focus on touchstart, touchmove, touchend corresponding logic, other are relatively rare event in temporarily not to discuss

touchstart

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

Copy the code

To get to the subsequent logic of the TouchStart event handler, a few conditions need to be met. What are the conditions? Let’s see what isPointerEventType and isPrimaryTouch do.

**isPointerEventType

function isPointerEventType(e, type){
  return (e.type == 'pointer'+type ||
    e.type.toLowerCase() == 'mspointer'+type)
}

Copy the code

Click here for Pointer Event information

isPrimaryTouch

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

Copy the code

According to the MDN pointerType, the type can be mouse,pen,touch, and only if its value is touch and isPrimary is true.

Then go back to

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

Copy the code

It’s essentially filtering out non-touch events.

Touch point information compatibility processing

// If it is a pointerdown event then firstTouch is saved as e, otherwise e.touches is the first
firstTouch = _isPointerType ? e : e.touches[0]

Copy the code

Here only clear e.touches[0] processing logic, another kind of not quite understand, please have the knowledge of the students to inform, thank you.

Restoration end point coordinates

// Normally, this is cleared with touchend or Cancel. If the user blocks the default event, it may not be cleared, but why clear the endpoint?
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
}

Copy the code

Store touch point partial information

// Save the current time
now = Date.now()
// Save the time between two clicks, mainly used for double click events
delta = now - (touch.last || now)
// touch.el Saves the target node
// If it is not a label node, use the parent node of that node. Note the false elements
touch.el = $('tagName' in firstTouch.target ?
  firstTouch.target : firstTouch.target.parentNode)
// If touchTimeout exists, clear it to avoid repeated triggering
touchTimeout && clearTimeout(touchTimeout)
// Record the starting point (x1, y1) (x, y)
touch.x1 = firstTouch.pageX
touch.y1 = firstTouch.pageY

Copy the code

Determine the double click event

// If the interval between two clicks is > 0 and < 250 ms, it is treated as a doubleTap event
if (delta > 0 && delta <= 250) touch.isDoubleTap = true

Copy the code

Handle the long press event

// Set now to touch.last so that it can calculate the time difference between two clicks
touch.last = now
// longTapDelay(750 ms) triggers the long press event
longTapTimeout = setTimeout(longTap, longTapDelay)

Copy the code

touchmove

.on('touchmove MSPointerMove pointermove'.function(e){
  if((_isPointerType = isPointerEventType(e, 'move')) &&
    !isPrimaryTouch(e)) return
  firstTouch = _isPointerType ? e : e.touches[0]
  // Cancel the long press event, both moved, of course not long press
  cancelLongTap()
  // end coordinates (x2, y2)
  touch.x2 = firstTouch.pageX
  touch.y2 = firstTouch.pageY
  // Record the changes in X and Y axes respectively
  deltaX += Math.abs(touch.x1 - touch.x2)
  deltaY += Math.abs(touch.y1 - touch.y2)
})

Copy the code

When the finger moves, it does three things.

  1. Cancel the long press event
  2. Record terminal coordinates
  3. Record the change in movement in x and y axes

touchend

.on('touchend MSPointerUp pointerup'.function(e){
  if((_isPointerType = isPointerEventType(e, 'up')) &&
    !isPrimaryTouch(e)) return
  // Cancel the long press event
  cancelLongTap()
  If the distance between the start point and the end point of the X-axis or Y-axis is more than 30, it is considered to be a slide, and the slide (SWIP) event is triggered.
  SwipLeft, swipRight, swipUp, swipDown
  // swipe
  if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
      (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))

    swipeTimeout = setTimeout(function() {
      if (touch.el){
        touch.el.trigger('swipe')
        touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
      }
      touch = {}
    }, 0)
  // The last attribute of the touch object is added to the touchStart event, so the start event will exist if triggered
  // 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
    // The tap event is considered possible only when the X and Y changes are less than 30
    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)
        // Create a custom event
        var event = $.Event('tap')
        // Add the cancelTouch callback to the custom event so that the user can cancel all events using this method
        event.cancelTouch = cancelAll
        // [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
        Trigger tap custom event when target element exists
        if (touch.el) touch.el.trigger(event)

        // trigger double tap immediately
        // If it is a doubleTap event, it is triggered and the touch is cleared
        if (touch.isDoubleTap) {
          if (touch.el) touch.el.trigger('doubleTap')
          touch = {}
        }

        // trigger single tap after 250ms of inactivity
        // Otherwise after 250 milliseconds. Trigger click event
        else {
          touchTimeout = setTimeout(function(){
            touchTimeout = null
            if (touch.el) touch.el.trigger('singleTap')
            touch = {}
          }, 250)}},0)}else {
      // Not tap-related events
      touch = {}
    }
    // Clear the change information
    deltaX = deltaY = 0

})

Copy the code

When the TouchEnd event is triggered, the corresponding comment is already there, but let’s break down the code.

Swip Events are related

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

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

Copy the code

After the finger leaves, by judging the displacement of the X axis or y axis, swIP and its corresponding direction events will be triggered as long as the span of one of them is greater than 30.

tap,doubleTap,singleTap

These three events can be fired only if the last attribute is still present in the touch object, and it is known from the touchstart event handler that last is recorded in it. The clearing time before touchend is when the longTap event is triggered and cancelAll is called to cancelAll events

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
    // [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
    if (touch.el) touch.el.trigger(event)
  }    
}
Copy the code

Note that Zepto also adds a cancelTouch attribute to the event object before the tap event is triggered. The corresponding cancelAll method allows you to cancelAll touch events.


// 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)}Copy the code

After a tap event is triggered, doubleTap will be triggered immediately, or singleTap will be triggered 250 milliseconds later, and the Touch object will be left empty for next use

// Clear the change information
deltaX = deltaY = 0

Copy the code

touchcancel


.on('touchcancel MSPointerCancel pointercancel', cancelAll)

Copy the code

Cancels all events when touchCancel is triggered.

scroll


$(window).on('scroll', cancelAll)

Copy the code

When a scroll event is triggered, cancel all events (tap, swIP).

At the end

Finally, say a question often asked in the interview, touch breakdown phenomenon. If you’re interested, check out the mobile click lag and Zepto penetration, the first issue of the New Year — the zepto Tap breakdown issue

reference

  1. Click latency and Zepto penetration on mobile

  2. First issue of the New Year — Zepto Tap breakdown

  3. Read Zepto source Touch module

  4. pointerType

  5. Html5 Pointer Event Api that integrates mouse, touch and stylus events

The article directories

  • touch.js

    • Swipe, Tap, longTap and other custom events (2017-12-22)
  • ie.js

    • Zepto source Code analysis of IE module (2017-11-03)
  • data.js

    • Zepto Data Caching principle and Implementation (2017-10-03)
  • form.js

    • Zepto source Code analysis form module (2017-10-01)
  • zepto.js

    • A collection of useful methods in Zepto (2017-08-26)
    • Zepto Core Module tools and Methods (2017-08-30)
    • Zepto: How to add, delete, alter, and query DOM
    • Zepto handles element attributes like this (2017-11-13)
    • Learn more about offset from Zepto (2017-12-10)
  • event.js

    • Why are mouseenter and Mouseover so intertwined? (2017-06-05)
    • How to trigger DOM events manually (2017-06-07)
    • Who says you just “know how to use “jQuery? (2017-06-08)
  • ajax.js

    • Jsonp (Principle and implementation details)