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.