Alloyfinger is a very lightweight open source gesture library that is widely used due to its lightweight, native JS-based features. As for its principle, its official team explained in great detail – portal. I’m not going to go into detail here.
Its core code is only more than 300 lines, completed 14 gestures, gestures are not browser native events, but by listening to the touchstart, Touchmove, touchend, touchCancel four native browser events hack out gestures, so its usage may be a little different from the native. For example, prevent default events, prevent bubbling, can not be used as native events.
Official code in addition to the core library of Alloyfinger react, vue implementation. Only the core library, the VUE version, is parsed here.
The core library:
/ * AlloyFinger By dntzhang v0.1.7 * * lot: https://github.com/AlloyTeam/AlloyFinger * Note By keenjaan * lot: https://github.com/keenjaan */
; (function () {
// A mathematical formula for calculating distances, angles, etc
// Calculate the length of the hypotenuse of a right triangle according to the length of both sides.
function getLen(v) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
// It is used to calculate the Angle between two gesture states
function dot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
// Calculate the Angle between two gesture states
function getAngle(v1, v2) {
var mr = getLen(v1) * getLen(v2);
if (mr === 0) return 0;
var r = dot(v1, v2) / mr;
if (r > 1) r = 1;
return Math.acos(r);
}
// Calculate the rotation direction of the included Angle, (counterclockwise is greater than 0, clockwise is less than 0)
function cross(v1, v2) {
return v1.x * v2.y - v2.x * v1.y;
}
// Convert the Angle to radians, and the absolute value
function getRotateAngle(v1, v2) {
var angle = getAngle(v1, v2);
if (cross(v1, v2) > 0) {
angle *= - 1;
}
return angle * 180 / Math.PI;
}
// The constructor used to handle the gesture listener
var HandlerAdmin = function(el) {
this.handlers = []; // Listen for the list of functions
this.el = el; // Listen on elements
};
// add a listener to the constructor
HandlerAdmin.prototype.add = function(handler) {
this.handlers.push(handler);
}
// the constructor removes the listener method
HandlerAdmin.prototype.del = function(handler) {
if(! handler)this.handlers = []; // When handler is false, it clears the list of listener functions
for(var i=this.handlers.length; i>=0; i--) {
if(this.handlers[i] === handler) {
this.handlers.splice(i, 1); }}}// Triggers the user event listening callback function
HandlerAdmin.prototype.dispatch = function() {
for(var i=0,len=this.handlers.length; i<len; i++) {
var handler = this.handlers[i];
if(typeof handler === 'function') handler.apply(this.el, arguments); }}// instantiate the object that handles the listener function
function wrapFunc(el, handler) {
var handlerAdmin = new HandlerAdmin(el);
handlerAdmin.add(handler); // Add a listener
return handlerAdmin; // Return the instance
}
// Gesture constructor
var AlloyFinger = function (el, option) {
this.element = typeof el == 'string' ? document.querySelector(el) : el; // Bind the element of the event
// This object of the start, move, end, cancel functions on the binding prototype is an AlloyFinger instance
this.start = this.start.bind(this);
this.move = this.move.bind(this);
this.end = this.end.bind(this);
this.cancel = this.cancel.bind(this);
// Bind the native TouchStart, TouchMove, TouchEnd, touchCancel events.
this.element.addEventListener("touchstart".this.start, false);
this.element.addEventListener("touchmove".this.move, false);
this.element.addEventListener("touchend".this.end, false);
this.element.addEventListener("touchcancel".this.cancel, false);
// Save the difference between two fingers when there are more than two fingers, used to calculate the distance between two points
this.preV = { x: null.y: null };
this.pinchStartLen = null; // The distance between two fingers
this.zoom = 1; // Initial scaling
this.isDoubleTap = false; // Whether to double-click
var noop = function () {};// Empty function, passed in when there is no binding event
// For 14 gestures, instantiate listener object respectively, add related listener function according to the value of option, add empty function if no.
this.rotate = wrapFunc(this.element, option.rotate || noop);
this.touchStart = wrapFunc(this.element, option.touchStart || noop);
this.multipointStart = wrapFunc(this.element, option.multipointStart || noop);
this.multipointEnd = wrapFunc(this.element, option.multipointEnd || noop);
this.pinch = wrapFunc(this.element, option.pinch || noop);
this.swipe = wrapFunc(this.element, option.swipe || noop);
this.tap = wrapFunc(this.element, option.tap || noop);
this.doubleTap = wrapFunc(this.element, option.doubleTap || noop);
this.longTap = wrapFunc(this.element, option.longTap || noop);
this.singleTap = wrapFunc(this.element, option.singleTap || noop);
this.pressMove = wrapFunc(this.element, option.pressMove || noop);
this.touchMove = wrapFunc(this.element, option.touchMove || noop);
this.touchEnd = wrapFunc(this.element, option.touchEnd || noop);
this.touchCancel = wrapFunc(this.element, option.touchCancel || noop);
this.delta = null; // The timestamp used to determine whether it is a double-click
this.last = null; // Record the timestamp variable
this.now = null; // Record the timestamp variable
this.tapTimeout = null; // Timer of the tap event execution
this.singleTapTimeout = null; // Timer executed by singleTap
this.longTapTimeout = null; // The timer executed by longTap
this.swipeTimeout = null; // Swipe executes the timer
this.x1 = this.x2 = this.y1 = this.y2 = null; // start (x1, y1) move (x2, y2
this.preTapPosition = { x: null.y: null }; // Remember the coordinates of the start finger
};
AlloyFinger.prototype = {
start: function (evt) {
if(! evt.touches)return; // touches手指列表,没有就return
this.now = Date.now(); // Record the current event point
this.x1 = evt.touches[0].pageX; // first finger x coordinates
this.y1 = evt.touches[0].pageY; // first finger y coordinates
this.delta = this.now - (this.last || this.now); / / timestamp
this.touchStart.dispatch(evt); // Trigger the touchStart event
if (this.preTapPosition.x ! = =null) {
// When the screen is not touched for the first time, compare the time interval between two touches. The time interval between two touches is less than 250ms and the distance between touch points is less than 30px.
this.isDoubleTap = (this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30);
}
this.preTapPosition.x = this.x1; // Save the touch coordinates to preTapPosition.
this.preTapPosition.y = this.y1;
this.last = this.now; // Record the touch time
var preV = this.preV, // Get the difference between the two points recorded
len = evt.touches.length; // Number of fingers
if (len > 1) { // The number of fingers is greater than 1
this._cancelLongTap(); // Cancel the longTap timer
this._cancelSingleTap(); // Cancel the singleTap timer
var v = { x: evt.touches[1].pageX - this.x1, y: evt.touches[1].pageY - this.y1 };
// Calculate the horizontal and vertical difference between two fingers and save it in the prev object, as well as in this.prev.
preV.x = v.x;
preV.y = v.y;
this.pinchStartLen = getLen(preV); // Calculate the distance between two fingers
this.multipointStart.dispatch(evt); // Trigger the multipointStart event
}
// Enable the longTap event timer. If the timer is not cleared within 750ms, the longTap event will be triggered.
this.longTapTimeout = setTimeout(function () {
this.longTap.dispatch(evt);
}.bind(this), 750);
},
move: function (evt) {
if(! evt.touches)return;
var preV = this.preV, // The difference between two points saved in the start method.
len = evt.touches.length, // Number of fingers
currentX = evt.touches[0].pageX, // The x-coordinate of the first finger
currentY = evt.touches[0].pageY; // The y coordinate of the first finger
this.isDoubleTap = false; // If it is moved, it cannot be a double click event
if (len > 1) {
// Get the difference between the current two points.
var v = { x: evt.touches[1].pageX - currentX, y: evt.touches[1].pageY - currentY };
// start The preV saved is not empty and pinchStartLen is greater than 0
if(preV.x ! = =null) {
if (this.pinchStartLen > 0) {
// Divide the distance between the current two points by the distance between the two points in start, calculate the scaling ratio, mount to evT object
evt.zoom = getLen(v) / this.pinchStartLen;
this.pinch.dispatch(evt); // Trigger the PINCH event
}
evt.angle = getRotateAngle(v, preV); // Calculate the rotation Angle, mount to evT object
this.rotate.dispatch(evt); // Trigger the rotate event
}
preV.x = v.x; // Assign the difference between the two fingers in move to preV, and also change this.prev
preV.y = v.y;
} else {
// Make a one-finger pressMove gesture
This. x2 is null when move is triggered for the first time. This. x2 is assigned after move.
if (this.x2 ! = =null) {
// Subtract the last move coordinate from this move coordinate to get the move distance in the x and y directions.
evt.deltaX = currentX - this.x2;
evt.deltaY = currentY - this.y2;
} else {
// Set evt.deltaX,evt.deltaY to 0.
evt.deltaX = 0;
evt.deltaY = 0;
}
// Trigger the pressMove event
this.pressMove.dispatch(evt);
}
// Trigger the touchMove event to mount different attributes to the EVT object and throw them to the user
this.touchMove.dispatch(evt);
// Cancel the long press timer. The long press event can be prevented within 750ms.
this._cancelLongTap();
this.x2 = currentX; // Record the current first finger coordinates
this.y2 = currentY;
if (len > 1) {
evt.preventDefault(); // More than two fingers prevent default events}},end: function (evt) {
if(! evt.changedTouches)return;
// Cancel the long press timer. The long press event will be blocked within 750ms
this._cancelLongTap();
var self = this; // Save this object.
// The multipointEnd event is raised if the remaining hand index is less than 2
if (evt.touches.length < 2) {
this.multipointEnd.dispatch(evt);
}
// the presence of this.x2 or this.y2 triggers the move event.
// math.abs (this.x1 - this.x2) represents the distance moved in the x direction.
Swipe event is triggered when a movement in the X or Y direction is greater than 30px
if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) | | (this.y2 && Math.abs(this.y1 - this.y2) > 30)) {
// Calculate the direction of swipe and write the evt object.
evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2);
this.swipeTimeout = setTimeout(function () {
self.swipe.dispatch(evt); // Asynchronously trigger the swipe event
}, 0)}else {
this.tapTimeout = setTimeout(function () {
self.tap.dispatch(evt); // Trigger the TAP event asynchronously
// trigger double tap immediately
if (self.isDoubleTap) { // if the double click condition is satisfied in the start method
self.doubleTap.dispatch(evt); // Trigger the double-click event
clearTimeout(self.singleTapTimeout); // Clear singleTap event timer
self.isDoubleTap = false; // Reset the double-click condition}},0)
if(! self.isDoubleTap) {// If the double-click condition is not met
self.singleTapTimeout = setTimeout(function () {
self.singleTap.dispatch(evt); // Trigger the singleTap event
}, 250); }}this.touchEnd.dispatch(evt); // Trigger the touchEnd event
// reset the relevant variable after end
this.preV.x = 0;
this.preV.y = 0;
this.zoom = 1;
this.pinchStartLen = null;
this.x1 = this.x2 = this.y1 = this.y2 = null;
},
cancel: function (evt) {
// Disable all timers
clearTimeout(this.singleTapTimeout);
clearTimeout(this.tapTimeout);
clearTimeout(this.longTapTimeout);
clearTimeout(this.swipeTimeout);
this.touchCancel.dispatch(evt);
},
_cancelLongTap: function () {
clearTimeout(this.longTapTimeout); // Turn off the longTap timer
},
_cancelSingleTap: function () {
clearTimeout(this.singleTapTimeout); // Turn off the singleTap timer
},
_swipeDirection: function (x1, x2, y1, y2) {
// Judge the direction of swipe
return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')},// Add a listener function to one of the 14 gestures
on: function(evt, handler) {
if(this[evt]) { // The event name is in the 14 before the function is added to the listening event
this[evt].add(handler); }},// Remove the listener for one of the 14 gestures
off: function(evt, handler) {
if(this[evt]) { // If the event name is in the 14, remove the corresponding listener
this[evt].del(handler); }},// Reset all data
destroy: function() {
// Disable all timers
if(this.singleTapTimeout) clearTimeout(this.singleTapTimeout);
if(this.tapTimeout) clearTimeout(this.tapTimeout);
if(this.longTapTimeout) clearTimeout(this.longTapTimeout);
if(this.swipeTimeout) clearTimeout(this.swipeTimeout);
// Remove four events from touch
this.element.removeEventListener("touchstart".this.start);
this.element.removeEventListener("touchmove".this.move);
this.element.removeEventListener("touchend".this.end);
this.element.removeEventListener("touchcancel".this.cancel);
// Clear the listener function for all gestures
this.rotate.del();
this.touchStart.del();
this.multipointStart.del();
this.multipointEnd.del();
this.pinch.del();
this.swipe.del();
this.tap.del();
this.doubleTap.del();
this.longTap.del();
this.singleTap.del();
this.pressMove.del();
this.touchMove.del();
this.touchEnd.del();
this.touchCancel.del();
// Reset all variables
this.preV = this.pinchStartLen = this.zoom = this.isDoubleTap = this.delta = this.last = this.now = this.tapTimeout = this.singleTapTimeout = this.longTapTimeout = this.swipeTimeout = this.x1 = this.x2 = this.y1 = this.y2 = this.preTapPosition = this.rotate = this.touchStart = this.multipointStart = this.multipointEnd = this.pinch = this.swipe = this.tap = this.doubleTap = this.singleTap = this.pressMove = this.touchMove = this.touchEnd = this.touchCancel = null;
return null; }};Export the AlloyFingerPlugin module if the current environment supports es6 syntax like module, exports, etc
if (typeof module! = ='undefined' && typeof exports === 'object') {
module.exports = AlloyFinger;
} else { // Otherwise register AlloyFingerPlugin with the global object
window.AlloyFinger = AlloyFinger;
}
})();
Copy the code
Vue version code:
/* AlloyFinger v0.1.0 for Vue * By june01 * Github: https://github.com/AlloyTeam/AlloyFinger * Note By keenjaan * Github: https://github.com/keenjaan */
; (function() {
var AlloyFingerPlugin = {
// The install function for the vUE mount directive
install: function(Vue, options) {
// options Parameters passed when the instruction is mounted
options = options || {};
// AlloyFinger is retrieved globally.
var AlloyFinger = window.AlloyFinger || options.AlloyFinger;
// Determine the vUE version
varisVue2 = !! (Vue.version.substr(0.1) = =2);
// Unable to get the AlloyFinger exception
if(! AlloyFinger) {throw new Error('you need include the AlloyFinger! ');
}
// Gesture name in 14
var EVENTMAP = {
'touch-start': 'touchStart'.'touch-move': 'touchMove'.'touch-end': 'touchEnd'.'touch-cancel': 'touchCancel'.'multipoint-start': 'multipointStart'.'multipoint-end': 'multipointEnd'.'tap': 'tap'.'double-tap': 'doubleTap'.'long-tap': 'longTap'.'single-tap': 'singleTap'.'rotate': 'rotate'.'pinch': 'pinch'.'press-move': 'pressMove'.'swipe': 'swipe'
};
// The record element adds an array of listening events.
var CACHE = [];
// Create an empty object to store the parameter objects of the vue custom directive
var directiveOpts = {};
If an element exists in the CACHE, index is returned; if an element does not exist, null is returned
var getElemCacheIndex = function(elem) {
for(var i=0,len=CACHE.length; i<len; i++) {
if(CACHE[i].elem === elem) {
returni; }}return null;
};
// Bind or unbind event listeners
var doOnOrOff = function(cacheObj, options) {
var eventName = options.eventName; / / the event name
var elem = options.elem; // Listen on elements
var func = options.func; // Listen to the function
var oldFunc = options.oldFunc; // The old listener function is used when dom updates
// If an event was added to the element
if(cacheObj && cacheObj.alloyFinger) {
If oldFunc exists, unbind the function oldFunc from the last binding.
if(cacheObj.alloyFinger.off && oldFunc) cacheObj.alloyFinger.off(eventName, oldFunc);
// If func exists, it is bound to func for both initialization and DOM updates
if(cacheObj.alloyFinger.on && func) cacheObj.alloyFinger.on(eventName, func);
} else {
// If no event has been added to the element
options = {}; // Create an empty object
options[eventName] = func; // Add a listener function to listen for events
// Add a listener element to the CACHE and its listener events and functions
CACHE.push({
elem: elem,
alloyFinger: new AlloyFinger(elem, options) // Initializes the AlloyFinger binding event}); }};// vue custom instruction initialization function
var doBindEvent = function(elem, binding) {
var func = binding.value; // Listen to the function
var oldFunc = binding.oldValue; // The old listener function
var eventName = binding.arg; // The name of the event to listen to
eventName = EVENTMAP[eventName]; // Convert event name to hump method
var cacheObj = CACHE[getElemCacheIndex(elem)]; // Get whether an element has event listeners added to the CACHE.
// Triggers the binding or removal of the event listener function
doOnOrOff(cacheObj, {
elem: elem,
func: func,
oldFunc: oldFunc,
eventName: eventName
});
};
// Remove the event listener
var doUnbindEvent = function(elem) {
var index = getElemCacheIndex(elem); // Get elem index in CACHE
if(!isNaN(index)) { // If the element exists in the CACHE
var delArr = CACHE.splice(index, 1); // Delete this listening event
if(delArr.length && delArr[0] && delArr[0].alloyFinger.destroy) {
delArr[0].alloyFinger.destroy(); // Reset the gesture alloyFinger object, stop all timers, remove all listener functions, and clear all variables.}}};// Determine the vUE version
if(isVue2) { // vue2
/ / directive parameter
directiveOpts = {
bind: doBindEvent,
update: doBindEvent,
unbind: doUnbindEvent
};
} else { // vue1
// vue1.xx
directiveOpts = {
update: function(newValue, oldValue) {
var binding = {
value: newValue,
oldValue: oldValue,
arg: this.arg
};
var elem = this.el;
doBindEvent.call(this, elem, binding);
},
unbind: function() {
var elem = this.el;
doUnbindEvent.call(this, elem); }}}// definition
Vue.directive('finger', directiveOpts); // Bind the custom directive finger}}Export the AlloyFingerPlugin module if the current environment supports es6 syntax like module, exports, etc
if(typeof module! = ='undefined' && typeof exports === 'object') {
module.exports = AlloyFingerPlugin;
} else { // Otherwise register AlloyFingerPlugin with the global object
window.AlloyFingerVue = AlloyFingerPlugin;
}
})();
Copy the code
Here’s the whole code parsing, with a few problems:
1. Long press whether all events in end such as TAP, swipe, TouchEnd, singleTap, doubleTap need to be cancelled.
To cancel all events in end, add a field isLongTap that is set to true when the longTap event is triggered. Determine isLongTap in end, return if true, block all events in end, and reset isLongTap to false
Swipe event and doubleTap are delimited.The difference between swipe and tap in the source code is that the distance between swipe and tap is less than or equal to 30px in both x and Y directions, and the distance between swipe and tap is greater than 30px. DoubleTap, too, is less than or equal to 30px in both x and Y directions.
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
Copy the code
That is, set the page width to the ideal viewport for the device. In my actual project if I set it as above, 30px might be too large and cause the swipe event to be triggered to turn into tap event. As for how much, you can try it out and find a threshold that works for your team.
In addition, in actual mobile projects, we may not set your viewport like this, such as flexible adaptation of Taobao team. On ios, the page viewport is zoomed, while on Android, the ideal viewport is used (no zooming viewport), resulting in a different sliding distance of 30px to the screen. Swipe events can be triggered if you swipe a small distance on ios. In this case, you can’t use it directly. You need to use it in conjunction with your mobile adaptation library, and make adjustments to the alloyfinger source code.
Check out my article on mobile adaptationportal
Method 1: Directly read the zoom of viewport in the source code of Alloyfinger, set different values for different models, so that the swipe event is triggered on all models and the finger moves the same distance.
Method two: for the vUE version of the implementation, through the vUE custom instruction, in the hanging instruction, dynamic through the parameters.
Vue.use(AlloyFingerVue, option) // Pass it in with arguments.
Copy the code
The option object is retrieved in the Install function of AlloyFingerPlugin and injected into the Alloyfinger object, where the swipe boundary value is modified. Specific implementation of my source code has been implemented, notes written very clear, do not understand can ask me, source link see the end of the article.
3, prevent bubbling, because its events in addition to touchstart, Touchmove, Touchend, touchCancel four native events, the other are hack, so it can not be written in the listener function like the native event to prevent bubbling. Bubbling needs to be prevented in the corresponding native event. Bubbling can be prevented in the VUE version by passing in parameters when registering instructions. Such as:
v-finger:tap.stoppropagation
Copy the code
The StopPropagation field can be read in the doOnOrOff function using the MODIFIERS field and registered with the AlloyFinger object. Go to this field in the Alloyfinger object pair to determine if you need to prevent bubbling.
Advantages: It is very convenient to prevent bubbling by adding a modifier to bind events.
Disadvantages: Once bubbling is prevented, all events on the element are prevented from bubbling, and special handling is required if an event needs to bubble.
In view of the above three points, the official version has been modified. See portal for the source code
The official project vUE version is buggy
I recently encountered a problem in my project where some page button binding events failed. Finally found the problem, the official vue version has a bug.
When vue-Router is used to switch routes, the doUnbindEvent function is fired for all elements bound to events when the last page was destroyed, and multiple times when multiple events are bound to an element. For an element:
<div v-finger:tap="tapFunc" V-finger :long-tap="longTapFunc"> button </div>Copy the code
DoUnbindEvent function:
var doUnbindEvent = function(elem) {
var index = getElemCacheIndex(elem);
if ( index ) {
return true;
}
if(!isNaN(index)) {
var delArr = CACHE.splice(index, 1);
if(delArr.length && delArr[0] && delArr[0].alloyFinger.destroy) {
delArr[0].alloyFinger.destroy(); }}};Copy the code
When doUnbindEvent is triggered for the first time, index must return a number and the element will be removed from the CACHE.
When doUnbindEvent is triggered a second time, index returns null because the element has been deleted, and the if condition does not intercept null.
if(!isNaN(index)) {
//} delArr = cache.splice (index,1) = CACHE.splice(null.1) = CACHE.splice(0.1);
Copy the code
To always intercept the first element in the CACHE array.
During route switching, the previous page fires doUnbindEvent, and the new page fires doBindEvent. Both fires simultaneously, causing binding elements to be added to the CACHE array and elements to be removed from the CACHE array. When an element is bound to multiple events, a null index removes the newly bound event for the new page element. The new page binding event fails.