Blog-Archiv

Donnerstag, 11. Juni 2015

JS Swipe Gesture

Mobile devices like Smartphones bring new life into the world of events. There are user gestures that can not be performed with a computer mouse, for instance zooming in and out with two fingers. This is called "pinch zoom", and it is also available for the touchpad of laptops. (Generally the touchpad is closest to what is a mobile input device.)

On a desktop computer, the mousewheel could be used to zoom. But when you look at the inner life of event processing, you see that the dispatch of mouse wheel events is completely different from that of two-finger touch events. Finally both gestures magnify the screen contents by giving a flow of events, but generating that flow is subject to programming software that interprets the according device events.

In case of web applications running on mobiles we have (besides lots of Blogs like this :-) jQuery mobile that provides us with utilities:

  • tap (like mouse click)
  • taphold (long press, e.g. for selecting text)
  • scroll (moving the tapped finger to uncover hidden view areas)
  • swipe (moving the tapped finger fast to browse to next page)
  • ....

jQuery also provides an abstraction layer that allows to program against events that are dynamically adjusted to the target device. This is called

But here is the raw material for event processing:

the w3c specifies a standard for browser vendors how the JavaScript API for touch-devices should look like and act.

Let's make something out of it. This Blog is about a JS swipe detector that can be used for both mouse- and touch-devices, and event adapters for actually using it with both of them.

What's Swipe?

Swipe is a "Turn the Page" gesture, we want to see "next" or "previous" by quickly smearing over the mobile screen with one finger from left to right or right to left. In case "next" is below "current", it also could be an up- or down-movement.

On a desktop computer this would be a mouse-click into the scrollbar's empty area, to make it scroll a whole page up or down. Having a keyboard you would press Page-Up or Page-Down.

A swipe-event is quite similar to a scroll-event. Actually they differ just by velocity and some minimum distance. That means a swipe

  1. is a fast gesture, and it
  2. needs to span a minimum of pixels, else it will be a scroll

Enable a Gesture

Here is JS source code that installs a swipe gesture on the slide show I introduced in a past Blog.

      var installListeners = function() {

        ....

        var swipeCallback = function(swipeType) {
          if (swipeType === "left")
            forwardAction();
          else if (swipeType === "right")
            backwardAction();
        };
        swipeMouse(swipeCallback, controls.slideframe);
        swipeTouch(swipeCallback, controls.slideframe);
      }

At the same time this specifies what to achieve:

  • I want to have a function swipeMouse() for installing a mouse-swipe,
  • and a function swipeTouch() for installing a touch-swipe,
  • onto a given HTML DOM element, in this case controls.slideframe,
  • calling back to a given function swipeCallback when the gesture occurs,
  • and that function receives a String parameter swipeType telling the swipe direction.

Actually the swipe-callback then executes forwardAction() or backwardAction() to skip to next or previous slide.

It should be no problem to install a touch listener on a desktop browser, or a mouse listener on a mobile. You simply would not receive events that the device does not support.

Common Swipe Logic

The common swipe-logic that is used by both touch- and mouse-events is in a "stateful" JS object that holds following "state" (stateful means it has private variables that have different values at different times):

    /**
     * Install a swipe gesture detector upon given element,
     * calling back to given swipeCallback function.
     * @param swipeCallback function to call when gesture occurs.
     * @param element the element to watch for gesture.
     */
    var swipeDetector = function(swipeCallback, element) {     
      var startTime, startX, startY;
      var highSpeed = false;

      var that = {};

      /**
       * To be called by derivations with a concrete start-event.
       * @param startMoveX the x-coordinate of the start-event.
       * @param startMoveY the y-coordinate of the start-event.
       */
      that.setStartState = function(startMoveX, startMoveY) {
        ....
      };
      
      /**
       * To be called by derivations with a concrete move-event.
       * @param moveX the x-coordinate of the move-event.
       * @param moveY the y-coordinate of the move-event.
       * @param event to call preventDefault() upon when swipe speed is reached.
       */
      that.setMoveState = function(moveX, moveY, event) {
        ....
      };

      /**
       * To be called by derivations with a concrete end-event.
       * @param endMoveX the x-coordinate of the end-event.
       * @param endMoveY the y-coordinate of the end-event.
       */
      that.detectSwipe = function(endMoveX, endMoveY) {
        ....
      };

      ....

    }

This object remembers the start-time and -location in private variables when setStartState() is called. It assumes that the pending gesture will not be high-speed, but when it is, it will remember that and prevent the event default in that case, so that the view would not scroll on a mobile device.

Mind that the private vars (startTime, startX, ...) are available to the that object at any time, which means every time you call the swipeDetector() function, new instances of these variables will start to live, and will survive the termination of the function by being tied to the closure of the that object.

When having the start coordinates of a gesture, it is easy to calculate the distance traveled when the gesture ends. This is needed to establish a minimum distance for swipes.

When having the start coordinates and the start time of a gesture, we can calculate the speed of the gesture by

speed = distance / duration

As soon as the speed exceeds a certain threshold, thee highSpeed flag will be set and event default processing (scrolling) will be prevented. That way the view won't move on a mobile when you swipe.

If you do not want to implement this :-), here is the source of my swipe detector.

  (Click left arrow to see source code.)

Mind that this does nothing without some event adapters that call its public functions. All functions starting with "that." are such, visible to the outside world.

Event Adapters

Following facilitates the API events documented by the w3c to drive the swipe detector for mouse events on a desktop browser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    "use strict";

    var swipeMouse = function(swipeCallback, element) {
      var detector = swipeDetector(swipeCallback, element);
      element = detector.getSensor();
      
      element.addEventListener("mousedown", function(event) {
        detector.setStartState(event.pageX, event.pageY);
      });
      
      element.addEventListener("mousemove", function(event) {
        if (detector.isStarted())
          detector.setMoveState(event.pageX, event.pageY, event);
      });
      
      element.addEventListener("mouseup", function(event) {
        detector.detectSwipe(event.pageX, event.pageY);
      });
      element.addEventListener("mouseout", function(event) {
        detector.detectSwipe(event.pageX, event.pageY);
      });
    };

Reusage of the swipe-detector source code happens by delegation. The event adapter allocates an instance of swipeDetector and connects event listeners to it. The events that are fired by the browser generate swipe events then.

Following does the same for touch events on a mobile browser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    "use strict";

    var swipeTouch = function(swipeCallback, element) {
      var detector = swipeDetector(swipeCallback, element);
      element = detector.getSensor();
      
      var touchCallback = function(event, isStart, isEnd) {
        if (event.changedTouches.length === 1) {
          var touch = event.changedTouches[0];
          if (isStart)
            detector.setStartState(touch.pageX, touch.pageY);
          else if (isEnd)
            detector.detectSwipe(touch.pageX, touch.pageY);
          else
            detector.setMoveState(touch.pageX, touch.pageY, event);
        }
      };

      element.addEventListener("touchstart", function(event) {
        touchCallback(event, true, false);
      });
      
      element.addEventListener("touchmove", function(event) {
        touchCallback(event, false, false);
      });
      
      var touchEndCallback = function(event) {
        touchCallback(event, false, true);
      };
      element.addEventListener("touchend", touchEndCallback);
      element.addEventListener("touchcancel", touchEndCallback);
      
      return detector;
    };

Events are browser-specific, so don't blame me if this does not work on IE :-!


You can always go to my homepage to see the full source of this, and to try it out with different configuration parameters (that I dropped here for simplicity), with both your mobile phone and a desktop browser.




Keine Kommentare: