Mobile terminal has provided us touchstart, touchmove, touchcancel and touchend four native touch events. But in general, these events are rarely directly used, such as long press events need to be implemented by themselves. Many open source projects also implement these features, such as Zepto’s Touch module and hammer.js. This article will explain the common mobile terminal event and gesture implementation ideas and methods, encapsulate a simple mobile terminal gesture library. Several examples of this implementation are as follows:

Chat list instance

Comprehensive instance

If you want to see the zoom and rotate effect, you can click the link above or scan the QR code on your phone to see the effect

Common events and gestures

Tap: A click event, like the Click event and the native TouchStart event, or an event that fires somewhere in between.

Longtap: Long press event, triggered after the finger is held down for a period of time, such as long press to save pictures.

Dbtap: Double click event, two quick finger clicks, common such as double click picture to zoom out.

Move /drag: A swipe /drag gesture that refers to a finger being pressed down and moved without lifting up, similar to the original TouchMove event, which is common in mobile iPhone AssistiveTouch.

Swipe (Right/Left/Up/Down) : Also a swipe gesture, unlike move, which is triggered when the finger is lifted after move and reaches a certain distance. According to the different direction can be divided into swipeLeft swipeRight, swipeUp and swipeDown.

Pinch /zoom: a gesture used to pinch and zoom in with two fingers, often used to zoom in and out of a picture.

Rotate: Rotate with two fingers. It is used to rotate pictures.

demand

Knowing the above common events and gestures, we ended up with a library of gestures that met the following requirements

  • Implement all of the above events and gestures
  • Retain the native four basic event callbacks
  • Support for chain calls
  • Multiple processing callbacks are supported for the same event and gesture
  • Support event delegation
  • Do not rely on third-party libraries

Implementation ideas and code

1. Basic code structure

The name of the library is here called Gesture and exposed as GT on Windows. The following is the basic code structure

; (function(){
	function Gesture(target){
		// Initialize the code
	}
    Gesture.prototype = {
        // Code to implement various gestures
    }
	Gesture.prototype.constructor = Gesture;
	if (typeof module! = ='undefined' && typeof exports === 'object') {
	    module.exports = Gesture;
	 } else if (typeof define === 'function' && define.amd) {
	    define(function() { return Gesture; });
	 } else {
	    window.GT = Gesture;
	 }
})()

Copy the code

Where target is the target element bound at instantiation, and supports passing in strings and HTML elements

2. Implementation of constructors

The constructor handles fetching the target element, initializing the configuration and other parameters required, and binding the basic event.

  function Gesture(target) {
    this.target = target instanceof HTMLElement ? target : typeof target === "string" ? document.querySelector(target) : null; // Get the target element
    if(!this.target) return ; // Do not instantiate
	// Here are some parameters to instantiate
	/ /...

	// Bind the basic event. Note that this points to the event handler in Prototype
    this.target.addEventListener('touchstart'.this._touch.bind(this),false);
    this.target.addEventListener('touchmove'.this._move.bind(this),false);
    this.target.addEventListener('touchend'.this._end.bind(this),false);
    this.target.addEventListener('touchcancel'.this._cancel.bind(this),false);
  }

Copy the code

The following sections focus on prototype implementations that implement _touch,_move,_end, and _cancel, respectively.

3. One-finger events and gestures

Single event and finger gestures include: tap, dbtap, longtap, slide/move/drag and swipe.

  • Train of thought

When the finger begins to touch, the native TouchStart event is triggered to retrieve the finger-related parameters. Based on the requirements, the native TouchStart callback should be executed at this point, which is the first step. The following should then happen:

(1) The longtap event should be triggered when the finger does not leave and does not move (or a very small distance) for a period of time (here set to 800ms);

(2) The original touchMove event callback should be triggered first, then the custom slide event (named slide) should be triggered, and at the same time, the LongTap event should be cancelled;

(3) When the finger leaves the screen, the original Touchend event callback should be triggered at first, while the LongTap event trigger should be cancelled. The swipe gesture callback will be triggered if the distance of the finger changes beyond a certain range within a certain period of time (set to 300ms in this case), otherwise, If the finger is not lowered again, the TAP event should be emitted, and if the finger is lowered and raised again, the DBTAP event should be emitted and the TAP event should be cancelled

  • Code implementation

First add the following arguments to the constructor:


this.touch = {};// Record the finger just touched
this.movetouch = {};// Record the finger parameters that change during the movement
this.pretouch = {};// Since double clicking is involved, you need an object that records the last touch
this.longTapTimeout = null;// Used to trigger the long-press timer
this.tapTimeout = null;// The timer that triggers the click
this.doubleTap = false;// The timer is used to record whether the double click is executed
this.handles = {};// The object used to store the callback function

Copy the code

The following is the code and instructions for implementing the above ideas:


_touch: function(e){
      this.params.event = e;// Record the event object when touched,params is the callback parameter
      this.e = e.target; // The specific element of the touch
      var point = e.touches ? e.touches[0] : e;// Get the touch parameter
      var now = Date.now(); // The current time
	  // Record the finger position and other parameters
      this.touch.startX = point.pageX; 
      this.touch.startY = point.pageY;
      this.touch.startTime = now;
	  // Since there will be multiple touches, click events and double clicks are for single touches, so empty the timer first
      this.longTapTimeout && clearTimeout(this.longTapTimeout);
      this.tapTimeout && clearTimeout(this.tapTimeout);
	  this.doubleTap = false;
      this._emit('touch'); // Executes the native TouchStart callback, _emit being the method to execute, as defined later
      if(e.touches.length > 1) {
        // Handle multiple finger touches
      } else {
        var self= this;
        this.longTapTimeout = setTimeout(function(){// Start the long-press timer immediately after finger touch, and execute it after 800ms
          self._emit('longtap');// Executive press callback
          self.doubleTap = false;
          e.preventDefault();
        },800);
		Var ABS = math.abs; var ABS = math.abs;
        this.doubleTap = this.pretouch.time && now - this.pretouch.time < 300 && ABS(this.touch.startX -this.pretouch.startX) < 30  && ABS(this.touch.startY - this.pretouch.startY) < 30 && ABS(this.touch.startTime - this.pretouch.time) < 300; 
        this.pretouch = {// Update the information of the previous touch to current, for use by the next touch
          startX : this.touch.startX,
          startY : this.touch.startY,
          time: this.touch.startTime }; }},_move: function(e){
		var point = e.touches ? e.touches[0] :e;
	    this._emit('move');// The native TouchMove event callback
	    if(e.touches.length > 1) {//multi touch
	       // Multiple fingers touch
	    } else {
          var diffX = point.pageX - this.touch.startX,
              diffY = point.pageY - this. Touch. StartY;// Coordinates relative to the initial touch of the finger
			  this.params.diffY = diffY;
              this.params.diffX = diffX; 
          if(this.movetouch.x) {// Record the relative coordinates of the last movement
            this.params.deltaX = point.pageX - this.movetouch.x;
            this.params.deltaY = point.pageY - this.movetouch.y;
          } else {
			this.params.deltaX = this.params.deltaY = 0;
          }
          if(ABS(diffX) > 30 || ABS(diffY) > 30) {// All single-finger non-slide events are cancelled when the finger moves beyond 30
            this.longTapTimeout &&  clearTimeout(this.longTapTimeout);
            this.tapTimeout && clearTimeout(this.tapTimeout);
  		    this.doubleTap = false;
          }
          this._emit('slide'); // Perform a custom move callback
         // Update the moving finger parameter
          this.movetouch.x = point.pageX;
          this.movetouch.y = point.pageY; }},_end: function(e) {
      this.longTapTimeout && clearTimeout(this.longTapTimeout); // When the finger leaves, cancel the long press event
      var timestamp = Date.now();
      var deltaX = ~~((this.movetouch.x || 0) -this.touch.startX),
          deltaY = ~~((this.movetouch.y || 0) - this.touch.startY);
	  var direction = ' ';
      if(this.movetouch.x && (ABS(deltaX) > 30 || this.movetouch.y ! = =null && ABS(deltaY) > 30)) {/ / swipe gesture
        if(ABS(deltaX) < ABS(deltaY)) {
          if(deltaY < 0) {/ / draw
            this._emit('swipeUp')
            this.params.direction = 'up';
          } else { / / an underscore
            this._emit('swipeDown');
            this.params.direction = 'down'; }}else {
          if(deltaX < 0) {/ / left
            this._emit('swipeLeft');
            this.params.direction = 'left';
          } else { / / right row
            this._emit('swipeRight');
            this.params.direction = 'right'; }}this._emit('swipe'); / / draw
      } else {
        self = this;
        if(!this.doubleTap && timestamp - this.touch.startTime < 300) {// Single click away within 300ms, trigger click event
          this.tapTimeout = setTimeout(function(){
            self._emit('tap');
            self._emit('finish');// A callback after the event is processed
          },300)}else if(this.doubleTap){// if you click and leave again within 300ms, the double click event is triggered, but the click event is not triggered
          this._emit('dbtap');
          this.tapTimeout && clearTimeout(this.tapTimeout);
          this._emit('finish');
        } else {
          this._emit('finish'); }}this._emit('end'); // The native Touchend event
    },

Copy the code
  • Binding and execution of events

Above, the handles = {} parameter is defined in the constructor to store the event callback handler, and the _emit method is defined on the prototype to perform the callback. Because the callback function is passed in when used, you need to expose an ON method. Here are the initial requirements:

  • Multiple handlers can be passed in for the same gesture and event
  • Support for chain calls

Therefore, on and _emit are defined as follows:


 _emit: function(type){!this.handles[type] && (this.handles[type] = []);
      for(var i = 0,len = this.handles[type].length; i < len; i++) {
        typeof this.handles[type][i] === 'function' && this.handles[type][i](this.params);
      }
      return true;
    },
on: function(type,callback) {!this.handles[type] && (this.handles[type] = []);
  this.handles[type].push(callback);
  return this; // implement chain calls
},

Copy the code

At this point, except for a few minor details, the basic handling of single-finger events is complete. Instantiate with code like the following:


new GT('#target').on('tap'.function(){
  console.log('You did a click');
}).on('longtap'.function(){
  console.log('Long press operation');
}).on('tap'.function(params){
  console.log('Second tap processing');
  console.log(params);
})

Copy the code

4. Multi-finger gestures

Common multi-finger gestures are pinch and rotate.

  • Train of thought

When multiple fingers touch, obtain the information of two fingers, calculate the initial distance and other information, and then calculate the new parameters when moving and lifting, and calculate the multiple of amplification or contraction and rotation Angle by the parameters before and after. Here, the mathematics involved is more, the specific mathematics knowledge can be searched to understand (portal). Mainly for:

(1) Calculate the distance between two points (modulus of vector)

(2) Calculate the included Angle of two vectors (inner product of vectors and its geometric and algebraic definitions)

(3) Calculate the direction of the Angle between two vectors (the cross product of vectors)

Geometric definition:

Algebraic definition:

Among them

In there,

In two dimensions, z₁ and Z ₂ are 0, so

  • Several algorithm code implementation

// The magnitude of the vector
var calcLen = function(v) {
  / / formula
  return  Math.sqrt(v.x * v.x + v.y * v.y);
}

// Angle of two vectors (including direction)
var calcAngle = function(a,b){
  var l = calcLen(a) * calcLen(b),cosValue,angle;
  if(l) {
    cosValue = (a.x * b.x + a.y * b.y)/l;// Get the cosine of the Angle between the two vectors
    angle = Math.acos(Math.min(cosValue,1))// Get the Angle between the two vectors
    angle = a.x * b.y - b.x * a.y > 0 ? -angle : angle; // Get the direction of the included Angle (clockwise and counterclockwise)
    return angle * 180 / Math.PI;
  }
  return 0;
}

Copy the code
  • Code to achieve multi-finger gestures
    _touch: function(e){
      / /...
      if(e.touches.length > 1) {
        var point2 = e.touches[1];// Get the second finger information
        this.preVector = {x: point2.pageX - this.touch.startX,y: point2.pageY - this.touch.startY};// Calculate the vector coordinates of the touch
        this.startDistance = calcLen(this.preVector);// Compute the magnitude of the vector
      } else {
        / /...}},_move: function(e){
      var point = e.touches ? e.touches[0] :e;
      this._emit('move');
      if(e.touches.length > 1) {
        var point2 = e.touches[1];
        var v = {x:point2.pageX - point.pageX,y:point2.pageY - point.pageY};// Get the current vector during the slide
        if(this.preVector.x ! = =null) {if(this.startDistance) {
            this.params.zoom = calcLen(v) / this.startDistance;// Calculate the scaling factor using the vector modulus ratio
            this._emit('pinch');Perform the pinch gesture
          }
          this.params.angle = calcAngle(v,this.preVector);// Calculate the Angle
          this._emit('rotate');// Execute the rotate gesture
        }
		// Update the last vector to the current vector
        this.preVector.x = v.x;
        this.preVector.y = v.y;
      } else {
        / /...}},_end: function(e) {
      / /...
      this.preVector = {x:0.y:0};// Resets the coordinates of the previous vector
    }
Copy the code

After sorting out the idea, the gesture of multi-finger touch is relatively simple. At this point, the core of the whole gesture library is basically done. One remaining feature, as required, is support for event delegation, which is mainly modified in the _emit method and constructor.

// Add a selector
function Gesture(target,selector) {
  this.target = target instanceof HTMLElement ? target : typeof target === "string" ? document.querySelector(target) : null;
  if(!this.target) return ;
  this.selector = selector;// Store selectors
  / /...
}
var isTarget = function (obj,selector){
  while(obj ! =undefined&& obj ! =null&& obj.tagName.toUpperCase() ! ='BODY') {if (obj.matches(selector)){
      return true;
    }
    obj = obj.parentNode;
}
return false;
  }
Gesture.prototype. _emit =  function(type){!this.handles[type] && (this.handles[type] = []);
  // Execute only if the element firing the event is the target element
  if(isTarget(this.e,this.selector) || !this.selector) {
    for(var i = 0,len = this.handles[type].length; i < len; i++) {
      typeof this.handles[type][i] === 'function' && this.handles[type][i](this.params); }}return true;
}

Copy the code

5. Perfect the details

  • touchcancelThe callback

The current code for ‘TouchCancel’ is as follows:


 _cancel: function(e){
  this._emit('cancel');
  this._end();
},

Copy the code

I am not sure whether it is appropriate to execute the end callback at the time of Cancel, or whether there are other ways to deal with it. Please kindly advise from those who know.

  • touchendAfter the reset

Normally, after the touchend event callback is completed, you should reset the parameters of the instance, including params, touch information, etc., so write some parameter Settings into the _init function, and replace the corresponding part of the constructor with this._init().

_init: function() {
  this.touch = {};
  this.movetouch = {}
  this.params = {zoom: 1.deltaX: 0.deltaY: 0.diffX: 0.diffY:0.angle: 0.direction: ' '};
}
_end: function(e) {
 / /...
 this._emit('end');
 this._init();
}
Copy the code
  • Add other events

In the process of looking for information, I saw another gesture library AlloyFinger, which is produced by Tencent. Someone else’s library is after a lot of practice, so I looked at the source code to do a comparison, found that the implementation of the idea is similar, but in addition to support the gesture of this article also provides additional gestures, the main differences are the following:

  • Callbacks to events can be passed in as instantiation-time arguments or can be used withonMethod subsequent binding
  • Provides for unloading corresponding callbacksoffMethod and methods for destroying objectsdestroy
  • Chain calls are not supported
  • Event delegation is not supported
  • Gesture changes various parameters through extension in nativeeventOn objects, operability is high (but is that good or bad?)
  • When you move your fingersdeltaXanddeltaY, but not for this articlediffXanddiffYIt may be that these two parameters are not really useful
  • Tap event subdivided totap.singletap.doubletaP and qlongtap, will trigger after a long presssingletapThe event,swipeThere is no breakdown, but direction parameters are provided
  • Native event adds multi-finger touch callbackstwoFingerPressMove.multipointStart.multipointEnd

After contrast, decided to add the multi-finger touch native event callback. Multitouch,multimove, and add off and destroy.

_touch: function(e) {
	/ /...
  if(e.touches.length > 1) {
    var point2 = e.touches[1];
    this.preVector = {x: point2.pageX - this.touch.startX,y: point2.pageY - this.touch.startY}
    this.startDistance = calcLen(this.preVector);
    this._emit('multitouch');// Add this callback}}, _move:function(e) {
  / /...
  this._emit('move');
  if(e.touches.length > 1) {
    / /...
    this._emit('multimove');// Add this callback
    if(this.preVector.x ! = =null) {/ /...
    }
    / /...
  }
}
off: function(type) {
   this.handles[type] = [];
},
destroy: function() {
  this.longTapTimeout && clearTimeout(this.longTapTimeout);
  this.tapTimeout && clearTimeout(this.tapTimeout);
  this.target.removeEventListener('touchstart'.this._touch);
  this.target.removeEventListener('touchmove'.this._move);
  this.target.removeEventListener('touchend'.this._end);
  this.target.removeEventListener('touchcancel'.this._cancel);
  this.params = this.handles = this.movetouch = this.pretouch = this.touch = this.longTapTimeout =  null;
  return false;
},
Copy the code

With removeEventListenner, you pass in a reference to the original binding function, and the bind method itself returns a new function, so you need to make the following changes in the constructor:

  function Gesture(target,selector) {
    / /...
    this._touch = this._touch.bind(this);
    this._move = this._move.bind(this);
    this._end = this._end.bind(this);
    this._cancel = this._cancel.bind(this);
    this.target.addEventListener('touchstart'.this._touch,false);
    this.target.addEventListener('touchmove'.this._move,false);
    this.target.addEventListener('touchend'.this._end,false);
    this.target.addEventListener('touchcancel'.this._cancel,false);
  }

Copy the code
  • Increase the configuration

In practice, there may be special requirements for the default parameters. For example, the swipe event is defined to be 1000ms rather than 800ms, and the swipe move is 50px rather than 30, exposing a setting interface for a few special values and supporting chain calls. The corresponding value in the logic is changed to the corresponding parameter.


set: function(obj) {
  for(var i in obj) {
    if(i === 'distance') this.distance = ~~obj[i];
    if(i === 'longtapTime') this.longtapTime  = Math.max(500,~~obj[i]);
  }
  return this;
}

Copy the code

Usage:


new GT('#target').set({longtapTime: 700}).tap(function(){})

Copy the code
  • Resolve the conflict

It is found that finger swiping (including move, Slide,rotate,pinch, etc.) conflicts with the browser’s window scroll gesture. In general, e.preventDefault() is used to block the browser’s default behavior. Library through _emit method params. When performing a callback event for native event object, but use the params. Event. The preventDefault () to prevent the default behavior is not feasible. Therefore, the _EMIT method needs to be tuned to receive an additional argument from the native event object, which is executed within the callback parameter range, and optionally handles some default behavior. Modified as follows:

_emit: function(type,e){!this.handles[type] && (this.handles[type] = []);
  if(isTarget(this.e,this.selector) || !this.selector) {
    for(var i = 0,len = this.handles[type].length; i < len; i++) {
      typeof this.handles[type][i] === 'function' && this.handles[type][i](e,this.params); }}return true;
}

Copy the code

The call in the response library needs to be changed to the form this._emit(‘longtap’,e).

Modified to prevent default behavior when used with e.preventDefault(), for example


newGT(el).. on('slide'.function(e,params){
  el.translateX += params.deltaX;
  el.translateY += params.deltaY;
  e.preventDefault()
})

Copy the code

6. End result

The final result is as shown at the beginning of this article and can be viewed by clicking on the link below

Mobile click here for comprehensive examples

Mobile click here to see an example chat list

To view zooming and rotation, you can scan the QR code on your phone or click on the comprehensive example link to see the effect

All the source code and library usage documentation, you can click here to view

All the problem solving ideas and codes are for reference and study, welcome to point out the existing problems and can be improved.

In addition, all my articles on nuggets will be synchronized to my Github, and the content will be updated continuously. If you think it is helpful to you, thank you for giving me a star. If you have any questions, please feel free to contact me. Here are a few addresses to synchronize articles

1. Explain some properties and practices of the CSS in depth

2. Javscript related and some tools/library development ideas and source interpretation related