A jQuery Plugin for Touch Swiping – Part 1 of 2
This article will explain the steps to create a jQuery plugin that detects the horizontal swiping motion on touch devices such as the iPhone and Android-based devices. This article is the first in a two-part series. In this article, we will be creating an image carousel that can respond to the user’s input and change the position of the carousel accordingly. The second article will extend the plugin by adding swipe detection.
HTML & CSS
Before we go on to the JavaScript, let’s take a look at the HTML and CSS for the image carousel which will be used to demonstrate the Swiper plugin. The HTML is shown below.
<div style="width: 330px; height: 200px;">
<div id="target">
<div>
<div><img alt="" src="rexy.jpg" /></div>
<div><img alt="" src="xena.jpg" /></div>
<div><img alt="" src="xenaagain.jpg" /></div>
<div><img alt="" src="rexyagain.jpg" /></div>
</div>
</div>
</div>
Similarly, the carousel’s CSS is shown below.
img { /*100% width to scale the height proportionately*/
width: 100%;
margin: 0;
}
.frame {
width: 100%;
height: 100%;
border: 1px solid #ccc;
overflow: hidden;
position: relative;
}
.pictures {
position: absolute;
width: 400%; /*change accordingly*/
left: 0%;
}
.pictures:after {
content: "\0020";
display: none;
height: 0;
}
.pictures .pic {
width: 25%; /*change with respect to .pictures*/
float: left;
}
The inner container (.pictures
) is set to 400% to contain four images. Each image’s container (.pic
) is set to 25% so that the images end up at a width of 330 pixels. If you change the number of images or use absolute values instead of percentages, you’d want to change the width value of the .pictures
and .pic
elements accordingly.
The images are made to line up horizontally by floating to the left. The frame (.frame
) is made to show only one image at a time. With this setup, we can then “slide” the carousel by changing the left
property of the .pictures
<div>
element.
JavaScript
Here is the skeleton of the plugin:
(function ($) {
'use strict';
var Swiper = function (el, callbacks) {
}
$.fn.swiper = function (callbacks) {
if (typeof callbacks.swiping !== 'function') {
throw '"swiping" callback must be defined.';
}
this.each(function () {
var tis = $(this),
swiper = tis.data('swiper');
if (!swiper) { //i.e. plugin not invoked on the element yet
tis.data('swiper', (swiper = new Swiper(this, callbacks)));
}
});
};
}(jQuery));
This listing is boilerplate code for creating a jQuery plugin. The bulk of the complexity is handled by the internal class Swiper
, whose methods are not yet defined. Swiper
is responsible for reading the events produced by the browser and invoking the callback. The plugin is defined in a closure so that the Swiper
class will not be mistakenly overridden by external code. The plugin is also prevented from binding to an element more than one time by associating the instantiated Swiper
class with the swiper
data attribute.
var Swiper = function (el, callbacks) {
var tis = this;
this.el = el;
this.cbs = callbacks;
this.points = [0, 0];
//perform binding
this.el.addEventListener('touchstart', function (evt) {
tis.start(evt);
});
this.el.addEventListener('touchmove', function (evt) {
evt.preventDefault();
tis.move(evt);
});
};
In the above listing, the Swiper
constructor instantiates the object’s properties and event handlers. The points
property is a two-celled array that stores the starting position of the finger in the first cell, and the ending position in the second cell. We will see the usage of this array in subsequent listings. Its values are both initially zero.
The constructor binds the touchstart
and touchmove
events, and proxies the events to the corresponding methods in the Swiper
class. The touchstart
binding initializes the points
array with the initial position of the finger. The touchmove
binding gives us the movement of the finger, which we will pass to the callback function to offset the carousel accordingly.
Swiper.prototype.start = function (evt) {
if (evt.targetTouches && evt.targetTouches.length === 1) {
if (evt.targetTouches[0].offsetX) {
this.points[0] = evt.targetTouches[0].offsetX;
} else if (evt.targetTouches[0].layerX) {
this.points[0] = evt.targetTouches[0].layerX;
} else {
this.points[0] = evt.targetTouches[0].pageX;
}
//make initial contact with 0 difference
this.points[1] = this.points[0];
}
};
The listing above shows the start()
method, which takes the event and reads the set of touches generated on screen. In devices with multi-touch capability, which means nearly all modern smartphones and tablets, this property is an array storing the locations of all contact points with the screen. In this implementation, we are keeping track of one contact point as we are tracking a single swipe gesture which is done using one finger.
We are checking for the different properties of the touch event to accommodate the different implementations of the touch behaviour on different devices. This used to be required to make it work for different devices. Today, however, the devices that I have tested all generate the pageX
property.
Since we are checking only for a horizontal swipe gesture, we ignore the pageY
property. We also set the cells of the points
property to the same value so that the initial difference between the starting and ending points is zero.
The function binding for the touchmove
event and other helper methods are listed below.
Swiper.prototype.diff = function () {
return this.points[1] - this.points[0];
};
Swiper.prototype.move = function (evt) {
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());
this.points[0] = this.points[1];
}
};
The diff()
method simply calculates the difference between the last point (which changes as the user moves the finger) and the previous point. This is illustrated by the following figure.
The move()
method also checks through the different properties to get the right one for storing into the second cell of the points
property. After storing the value, the callback function is invoked with the difference between the previous position and the new position of the finger. The callback function is responsible for changing the position of the carousel. This is explained below.
After invoking the callback, the previous position’s value is replaced with the current position’s value. The next time the callback is invoked, the difference will be the displacement between the current position and the previous position instead of the starting position. This is required if we want the movement of the carousel to mirror that of the finger. Without this line, the carousel’s movement accumulates the difference and the result is a large displacement of images in response to a small movement of the finger, which is clearly undesirable for a smooth user experience.
The listing below invokes the plugin.
var target = $('#target'),
pictures = $('.pictures', target),
MAX_LEFT = -990,
MAX_RIGHT = 0,
currPos = 0,
cb = {
swiping: function (displacement) {
currPos += displacement;
if (currPos > MAX_RIGHT) {
currPos = MAX_RIGHT;
} else if (currPos < MAX_LEFT) {
currPos = MAX_LEFT;
}
pictures.css('left', currPos + 'px');
}
};
target.swiper(cb);
We get the element using its id
. We also need a handle to the .pictures
element within the target because the carousel’s positioning is changed by changing the left
CSS property of this element.
We set the left and right limit of the carousel’s position with the MAX_LEFT
and MAX_RIGHT
variables. These values have to change in relation to the carousel’s size. They are used so that the user does not scroll the carousel to empty spaces. The MAX_RIGHT
variable determines how far right the finger can drag the carousel to hit the left-most image. Naturally, this value is 0
. The MAX_LEFT
variable limits how far left the finger can move the carousel. Since there are four images, to display the right-most one, the three images on the left have to be displaced. The values are derived like this:
330 (width of one image) * 3 = 990
We also have a variable, currPos
, that stores the current position of the carousel. Alternatively, we can get the position of the carousel like so:
currPos = parseInt(pictures.css('left'));
The preferred approach is to use the variable. The only reason is that of performance – retrieving the left
property of the element and converting it into an integer definitely consumes more processing power than accessing a variable’s value. This is cognizant of the fact that we are adding behavior on top of a browser’s interface, so it is important that we keep the plugin lean.
The callback is specified as a property within a JSON literal. Why not simply pass it as a function? Well, this is to set the stage for part two of this series, where we will explain how to add swipe gesture detection to the plugin.
A final note: on iOS devices (iPhones and iPads), there is a bouncing effect on the browser window when you scroll the carousel. This is apparent if the carousel is near the bottom or the top (as is the case here) of the page. To prevent that from happening, we call the preventDefault()
method on the touchmove
event. Incidentally, by calling the preventDefault()
method, it prevents the event from bubbling up the DOM hierarchy, which in turn leads to better performance, especially apparent on slower devices such as the Nexus One. I’ve tested the plugin on the iPad 2 (iOS 6.0.1), Nexus One (Android 2.3.6), and Galaxy Note II (Android 4.1.2). If you have used any other devices/OS, feel free to let us know in the comments!