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
touchcancel
The 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.
touchend
After 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 with
on
Method subsequent binding - Provides for unloading corresponding callbacks
off
Method and methods for destroying objectsdestroy
- Chain calls are not supported
- Event delegation is not supported
- Gesture changes various parameters through extension in native
event
On objects, operability is high (but is that good or bad?) - When you move your fingers
deltaX
anddeltaY
, but not for this articlediffX
anddiffY
It may be that these two parameters are not really useful - Tap event subdivided to
tap
.singletap
.doubleta
P and qlongtap
, will trigger after a long presssingletap
The event,swipe
There is no breakdown, but direction parameters are provided - Native event adds multi-finger touch callbacks
twoFingerPressMove
.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