A jQuery Plugin for Touch Swiping – Part 2 of 2

Tweet

This is part two of two in the series describing how to create a jQuery plugin to detect and respond to swipe gestures.

In the first part, we saw how to create a plugin that changes a carousel’s pictures to correspond to the position of the finger on screen. In this part, we will extend that to detect a swipe gesture. This improves the carousel by creating fixed intervals so that a swipe changes the carousel to show the next/previous picture in its entirety.

var Swiper = function (el, callbacks, options) {
  ...
  this.opts = options;
  this.swipeDone = false;

  //perform binding
  this.el.addEventListener('touchend', function (evt) {
    tis.stop(evt);
  });
  ....
};

Swiper.LEFT = - 1;
Swiper.RIGHT = 1;

...

$.fn.swiper = function (callbacks, options) {
  var opts = $.extend({}, $.fn.swiper.defaults, options);
  if (typeof callbacks.swiping !== 'function') {
    throw '"swiping" callback must be defined.';
  }
  if (typeof callbacks.swiped !== 'function') {
    throw '"swiped" callback must be defined.';
  }
  if (typeof callbacks.swipeCancel !== 'function') {
    throw '"swipeCancel" callback must be defined.';
  }

  this.each(function () {
    ...
    if (!swiper) {
      tis.data('swiper', (swiper = new Swiper(this, callbacks, opts)));
    }
  });
};

$.fn.swiper.defaults = {
    tolerance: 100
};

In the above listing, we see that the class constructor for Swiper is modified to accept a third parameter options that holds a single property tolerance. The parameter is assigned to an internal property opts. The touchend event is proxied to the stop() instance method of the Swiper class.

Additionally, two callback functions have been added (swiped and swipeCancel) to handle the swiping. The default value for tolerance is set as 100 (pixels). Notice there are also two class properties Swiper.LEFT and Swiper.RIGHT. They represent a left swipe and a right swipe respectively and will be used in subsequent listings.

The plugin definition is also modified to accept a second, optional, parameter that provides the choice to override the default threshold of 100 pixels.

We need to extend the plugin to recognise when a swipe has occurred and modify the carousel correspondingly. There are several scenarios we need to consider. The first, and most straightforward, is when the user drags a finger across the screen and releases it. If the finger covers a distance that is more than or equal to the tolerance value, we consider that a swipe, and the carousel should advance to the next image when the finger is lifted off the screen. The figure below illustrates this scenario.

On the other hand, if the finger covers a distance that is less than the tolerance value, the movement is not considered a swipe and the carousel should revert to its original position.

Remember that the plugin moves the carousel to mirror the movement of the finger for the duration of contact with the screen. The decision as to whether to advance or revert the carousel position is taken when the finger is lifted off the screen (binding for the touchend event).

Swiper.prototype.stop = function (evt) {
  if (!this.swipeDone) {
    this.cbs.swipeCancel();
  } else {
    this.cbs.swiped(this.getDirection());
  }
};

If a swipe is detected, invoke the callback for advancing the carousel (swiped()), otherwise revert the carousel (swipeCancel()).

One thing to note is that we are returning the direction of the movement to the “advancing” callback as determined by the getDirection method. This is required as the callback needs to know which direction to advance the carousel to.

Swiper.prototype.getDirection = function () {
  var direction = this.diff();
  if (direction < 0) {
    return Swiper.LEFT;
  }
  if (direction > 0) {
    return Swiper.RIGHT;
  }
};

This method uses the diff() method defined in Part 1 of this series to get the displacement of the finger. If the difference is negative, it is a left swipe, otherwise it is a right swipe.

We now need to know how to determine whether a swipe is generated i.e. setting of the swipeDone flag. Before we delve into that though, let’s consider the next scenario.

If the user brings a finger to the screen, drags it beyond the threshold value, and then brings it back to within the threshold value before removing the finger, we don’t want to advance the carousel as the user’s intention, by bringing the finger back, is that he/she does not want to advance the carousel.

Similarly if the user brings the finger back beyond the tolerance value before removing the finger, his/her intention is to advance the carousel.

As you can imagine, the determination of when the gesture is detected is done while the finger is dragging on the screen, and not when the finger is lifted. We therefore have to make some changes to the move() instance method of the Swiper class.

Swiper.prototype.move = function (evt) {
  if (Math.abs(this.diff()) >= this.opts.tolerance) {
    this.swipeDone = true;
  } else {
    this.swipeDone = false;
  }
  if (evt.targetTouches && evt.targetTouches.length === 1) {
    if (evt.targetTouches[0].offsetX) {
      this.points[1] = evt.targetTouches[0].offsetX;
    } else if (evt.targetTouches[0].layerX) {
      this.points[1] = evt.targetTouches[0].layerX;
    } else {
      this.points[1] = evt.targetTouches[0].pageX;
    }
    this.cbs.swiping(this.diff());
  }
};

At the beginning of the move() method, we check if the distance moved by the finger has exceeded the tolerance value. The difference is encompassed in a Math.abs() function because a left movement will always generate a negative value which is less than any positive value. By taking its absolute value, we can check the distance for both left and right movements. If it is determined that the distance is larger than or equals to the tolerance value, we consider it a swipe.

A key point about making this work is the removal of the line this.points[0] = this.points[1]; from the move() method. This is absolutely critical because we want to take reference from the point where the finger came into contact with the screen (touchstart). If we retain this line of code, the reference point will keep changing with each movement of the finger and we will not be able to perform the calculation that we want. With the removal of this line of code though, the value that diff() returns will also be different from before. We then have to change the definition of the swiping() callback function.

One last change to make to the Swiper class is to its start() instance method. This change basically says that every time a finger is first placed on screen, the swipeDone flag to false, which is natural since a swipe cannot have been generated when the finger first touches the screen.

Swiper.prototype.start = function (evt) {
  if (evt.targetTouches && evt.targetTouches.length === 1) {
    this.swipeDone = false;
    ...
  }
};

We are pretty much done with the plugin itself. Changing the application code to utilise the plugin requires a paradigm shift in how we manipulate the carousel’s position while the finger is still dragging on screen. Remember that the carousel is supposed to “snap” to positions that show any of the images in its entirety. As a result, the value of the position is always a multiple of the width of each image. Thus it is easiest to represent the position as a percentage. Note that since we are treating the currPos value as a percentage, the values of MAX_LEFT and MAX_RIGHT need to be converted into percentages too.

We still want to retain the mirroring effect of the carousel on the finger. To do that, a new variable adhocPos is introduced in the swiping() callback. This value holds the position of the carousel as the finger drags on the screen. It uses the baseWidth variable which is set to 330 pixels (the width of each image in the carousel). This value must be changed if the width of an image in the carousel changes.

...
MAX_LEFT = -300,
MAX_RIGHT = 0,
baseWidth = 330;
cb = {
  swiping: function (displacement) {
    var adhocPos = currPos / 100 * baseWidth;
    adhocPos += displacement;
    pictures.css('left', adhocPos + 'px');
  },
  ...
}

The currPos value is treated as a percentage which is set in the swiped callback below:

swiped: function (direction) {
  currPos += (direction * 100);
  if (currPos < MAX_LEFT || currPos > MAX_RIGHT) {
    //i.e. exceeded limit
    currPos -= (direction * 100);
  }
  pictures.css('left', currPos + '%');
}

The callback is passed a direction parameter which, as we saw earlier, is either 1 or -1. This is then multiplied with 100 to convert into a percentage value before summing up with currPos. The if statement checks to make sure that the value remains within the boundaries so that the carousel does not scroll into empty space. This was previously done in the swiping() callback. By placing this check in the swiped() callback, we get the effect that when the user drags the carousel beyond the last image, we see white space but as soon as the finger is lifted, the carousel jumps back, thereby creating a sort of “bouncing” effect.

Lastly, there is the swipeCancel() callback which sets the carousel back to its original position before the dragging started.

swipeCancel: function () {
  pictures.css('left', currPos + '%');
}

With the modifications we made to the plugin in this article we have a decent swiping carousel that works almost like a native app in your browser. Similarly, the plugin has been tested in the same browsers as stated in the first article. You can see the demo or download the source code and have fun with them!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.