I recently implemented a swipe gesture to improve the user experience on this blog. If you're on an iOS or Android device you should be able to swipe across the content of this page to peer at the next and previous articles. I'll show you how it works.
No JS Framework, Just Native Methods
I've been using jQuery in a lot of my projects purely out of habit. But that's not really necessary when you're developing for touch devices. This time around I figured I'd skip the jQuery tax and only use third party libraries as needed. I also chose to code the project in CoffeeScript. So be advised, all of the example code here is CoffeeScript.
Rather than jQuery, I am only relying on three dependencies that total to 12k before gzipping:
The project itself consists of three files:
- Article.coffee is a model object that represents any page of this blog.
- ArticleController.coffee is a controller that manages which articles to present and or remove.
- PageSlider.coffee is more of a component than it is a view. It handles touch events applied to and applies CSS transforms to the current document.
Enough Jabbering! How can I Swipe Pages?
The first key to this puzzle is handling touch events from the device. I wrapped this up into a nifty class called PageSlider. Let's go through the constructor first:
The most important stuff happens on line 11 in the gist. First, I feature detect touch support with modernizr:
Then I add event listeners to the @touchTarget property of the class. In this case, @touchTarget is referencing the document.body. So any "touchstart", "touchmove", "touchend", or "touchcancel" event occurring on the body of the page is getting picked up by a given instance of the PageSlider class. One trick here -- when passing in the event handler I create an anonymous function using CoffeeScript's fat arrow (=>) operator for function binding.
This ensures the scope of 'this' remains the PageSlider and not the actual @touchTarget (which would be the document.body if you're still with me!)
What Happens When a User Touches the Screen?
Now that we've binded the events we need someway to handle them. The "touchstart" event will fire as soon as the user's device detects a new touch. It's the perfect time to do some critical setup:
The event object will return a property called "touches" so calling event.touches will return the number of fingers currently on the screen. I start off ensuring only one finger is on the screen before continuing:
This is a conscious effort by me to only support one finger for this interaction. You could easily adjust this if you wanted to support horizontal swiping with two or three fingers. But remember, if you go any higher you might trigger iOS's four finger swipe convention for switching apps.
What's really important in this method happens on lines 7-9. Here I'm resetting the direction of the page slider:
...and obtaining the initial X and Y location of the touch event:
These initial locations are important because we'll need to compare them to the new location of the user's finger as the user swipes in order to find out how far they've moved. As for the direction, I've just made a handful of class level constants that I could reference. Each direction constant stores an integer - here's what the code looks like:
Using class level constants like this is a great way to keep things DRY and also makes your code more readable. Imagine later on you need to check which direction the touch is moving, which of the following examples is easier to comprehend?
Tracking Touches as they Move
Ok so we have obtained the initial X and Y position of the user's touch. Now how do we react to the user as they move their finger? Let's take a look at the event handler for "touchmove":
This is a pretty long function and I've broken it down into a handful of gists to explain it in detail. First, I ensure the user is still only touching the screen with 1 finger. Then I calculate what I call the xOffset and yOffset. This is the x and y distance of the finger's current position compared to where it started.
Is the user scrolling or swiping? That's what we need to determine next in order to prevent scrolling if necessary. It's important not to trigger the swipe behavior when the user intends to scroll. Without some sort of threshold to differentiate between scrolling and swiping, the page will be jittering as the user scrolls up or down an article. However, when the user is actually swiping we'll want to call preventDefault() in order to disable scrolling. Here's how I did it:
That last line sets the current xOffset as a property on the PageSlider called @slideTo. We'll use that value to translate the article with CSS later. But first, this is where we'll determine the direction the user is swiping and broadcast a custom event. This will allow the ArticleController to determine which article to show just beneath the current article getting dragged.
That bit of code is really important in the big scheme of things here. The diagram below shows how the direction change event is broadcasted to the controller which determines which article should show up underneath the article currently being dragged.
Now that the correct article is set to appear from beneath the current page, we need to translate the document to make it move with the user's finger. The property @slideTo is storing the current xOffset and so we'll use this value to translate the current @article we're manipulating. Just to recap this is where we're at in the "touchMove" event:
The last line of the method actually sets the translation onto the article:
So what exactly does @article's translate() look like? Take a look:
The translate method takes an xPosition as its only parameter and then uses Modernizr to apply the appropriate CSS translation directly to the @article's element attribute. We want to default to 3D transforms if available in order to obtain better performance in webkit browsers. This is necessary purely for performance reasons which is why the translation does not using the Z-axis. To apply the transform to the style I use Modernizr's prefixed() method which is really useful when manipulating proprietary properties such as -webkit-transform.
What Happens when the Touching Stops?
To finish off our interaction we need to position the articles into their proper places once the user's finger is lifted from the display. If we do nothing, the article would simply stay wherever the user left it. For my blog I wanted to design an interaction where two things could happen:
- If the article was moved more than one third of the screen's width in either direction, then I want the article to continue sliding in that direction off the screen.
- Otherwise, if the article was left mostly in place, then I want the article to slide back into its original position.
This is where the touchEnd method comes into play:
The logic in the event handler itself is pretty straight forward. If the user dragged to the right (a positive value) we move the article off the screen to the right. Otherwise, if @slideTo is a negative value we tell the page to slide to the left. And, if @slideTo is within both limits we tell the page to move back to 0, its origin. But this method is relying on @moveTo(), a method on the PageSlider class:
There are a few things going on here. First off, we need to enable css transitions on the current page.
Both methods make use of Modernizr's prefixed() method to toggle this transition rule: "all 0.2s ease-out" on the DOM element containing the current article.
Back to that @moveTo() method:
Here's what it's doing:
- Enable CSS transitions on the article.
- Add an event listener that will fire once the transition finishes.
- Actually apply the desired translation to the article.
- Broadcast the PageSlider.DIRECTION_FINISHED custom event once the transition completes only if the page was moved out of the viewport.
The method is actually pretty short but we have to define an object using browser prefixed transition properties as attributes and browser prefixed TransitionEnd events as values:
This is because Modernizr's prefixed() doesn't support event names for whatever reason. So instead we use Modernizr to return the prefixed transition property and find the prefixed event from our object:
That's How the Pages Move But...
There's still a whole lot more happening than simply applying translations to the page. In part 2 I'll cover how I load the adjacent pages into the document and cache them in the browser to reduce the amount of request a user sends over. We'll also cover how to implement HTML5's pushState to update the address bar's URL and title for the current article without using a hash bang (#!). Stay tuned!
One last thing, I don't take comments here so if anything isn't clear please PLEASE message me on twitter and I'll reply to you. I'm @jimjeffers. Thanks for reading!