Creating Full-Page Mobile Swipe with AngularJS

Every website wants to have a flashy front page. It increases user interaction, raises revenue, and adds the all-important awesome factor to your site. For Zoosk, this means giving users the ability to swipe between search results, just like in a native app. But making one isn’t always the simplest task. As part of a recent project, I implemented the swipe ability, and the story is a long one.

The Idea

I started this project by thinking, “what would the most Angular way to do this be?”. The answer that I found was using a custom directive, like this:

<slideshow model="searchResultModel">
<!-- DOM for one profile -->
</slideshow>
view raw one.html hosted with ❤ by GitHub

The result of this code would be a full-page slideshow that would repeat the inner DOM three times. The middle slide would display the current search result, and swiping left or right would make the slides on the left and right visible. But how to do it?

Manipulating the DOM

AngularJS tries hard to keep controllers and DOM separate. The recommended solution for situations like this is a directive, but if you’ve tried changing the document structure in your directive’s link function, you’ll find that it doesn’t always work. Instead, you need to use the compile directive parameter.

'compile': function(tElement, tAttrs, transclude) {
// Any manipulation that you do here will happen before any of the link functions execute!
return linkFunction;
}
view raw two.js hosted with ❤ by GitHub

Using the compile function, it isn’t too hard to copy the innerHTML of the element and then append the appropriate DOM. The most important part, though, is saving your elements for later.

Rendering the correct result

Having the slideshow is cool, but how to make each slide display the correct search result? First of all, you need to get your hands on the children’s $scopes. And since you can’t guarantee that your child elements have a scope to begin with, you’re probably better off actually associating a new scope with each of your child slides as they are appended:

var newScope = $scope.$new();
var childSlideElement = $compile(childSlideHTML)(newScope);
view raw three.js hosted with ❤ by GitHub

This ensures that each of your slides has its own isolated scope and will remain working after it is appended to the slideshow container. Once that happens, you’ll be able to let each slide know about a different slideshow index, and have them handle the rest. You might want to simply:

var scope = firstChild.scope();
scope['slideshowIndex'] = 1;
view raw four.js hosted with ❤ by GitHub

Or, you could do what I did and use an event that will allow controllers to react to new indices:

scope.$broadcast('newIndex', 1);
view raw five.js hosted with ❤ by GitHub

Either way, after you’re all done you’ll have each slide displaying the correct information. Now all we have to do is make the swiping work!

Handling Gestures

What is probably the most important part of the Angular swipe experience ends up hardly using Angular code at all. Your code will differ depending on your CSS setup, but here’s the general idea of the code that you’ll put in your directive’s link function:

var slideWidth = 100, startPosition = -slideWidth, dragStartPosition, dragging = false;
element.on('mousedown', function(event) {
dragStartPosition = event.clientX;
dragging = true;
});
element.on('mousemove', function(event) {
setContainerPosition(startPosition + event.clientX - dragStartPosition);
});
element.on('mouseup', function(event) {
var delta = event.clientX - dragStartPosition;
// Do something with delta to snap to the correct slide
// Update the scopes of the child slides so that they display the correct results
});
view raw six.js hosted with ❤ by GitHub

Performance

At this point, I had a working slideshow. Hooray! Except that on my iPhone, it was about as quick as a paralyzed turtle. It turns out that the road to getting JS in a mobile browser to perform at native speeds isn’t pretty.

Problem 1: Mobile browsers don’t use the GPU by default

You’re probably familiar with the idea of a graphics card. It makes things look amazing, but it eats up battery power like a famished hippo. As a result, animations and transitions can be choppy in the browser. However, there is one thing that the GPU does do better: 3D transforms.

But wait, you say, we don’t want anything 3D to happen here. That’s why we’re going to trick the browser into rendering our slideshow element with the GPU by applying this transform:

-webkit-transform: translate3d(0, 0, 0);
-webkit-backface-visibility: hidden;
view raw seven.css hosted with ❤ by GitHub

This transform moves the page in 3D space… by 0 pixels. This produces no change, but it does cause the GPU to draw the element, which means that any translations applied to it will be smooth as silk. The line about backface-visibility causes the browser to skip drawing the back side of the page, which is useless to us.

Problem 2: Too many $watch statements

You may have noticed that AngularJS picks up changes almost magically. This is because of its system of watch statements, which goes over everything in the DOM and looks for changes after anything happens. This makes development very quick, but can lead to performance problems when implementing large pages.

Our architecture calls for having three pages all working at once, and all those watch statements contributed to a 650ms lag on finger release in the first implementation. For the second one, I created a new “dummy” page that looked like the search result page, but didn’t have anything but the display logic in it. These dummies were displayed as the slides adjacent to the middle one.

robin_post

This reduced the number of $watch statements by more than 50%, and performance subsequently went up.

 

Problem 3: Android mouse events

After the above optimizations, the site performed like a dream on iOS. Android, however, was a different story. The CSS transition seemed fine, but the dragging behavior was still laggy.

After doing some research, I found that there was a bug in Android mousemove events. Particularly, that they don’t fire nearly as quickly (sometimes 5x) as iOS.

The upshot of this is that when dragging, the view wouldn’t update its position nearly as quickly, preventing anyone from experiencing a good frame rate. With a little more research, though, I found the solution:

element.on('mousemove', function(event) {
event.preventDefault();
// The rest of your tracking code goes here
});
view raw eight.js hosted with ❤ by GitHub

Problem 4: Android in general

Even after the above improvement, Android phones seemed to underperform when compared to iOS. So I went into research mode, and came up with these rather unfortunate articles on Android performance and OS adoption rates. Apparently, the majority of Android users are not only using technology that’s slower than the competition, but they’re also neglecting important upgrades.

In a last-ditch effort to improve performance for the old platform, I added three new optimizations:

  1. I removed all Closure code from my event handlers. We use Google Closure at Zoosk, but some of the library functions access the DOM, which is a big no-no for animations. I therefore removed all Closure code and added my own, removing unnecessary asserts, DOM accesses, and more.
  2. I cached the entire state of the feature in variables instead of DOM accesses. DOM access is very slow compared to variable lookups, and I didn’t want to have to suffer the cost anywhere, even when it wasn’t during any of the animations.
  3. I added loading heuristics to the slides. With the old code, we would always have three slides available. With the new code, we would record the direction that the user was swiping, and then only load new information in that direction until the user turned around. This reduced the in-swipe lag.

With all of these upgrades, we finally got our swipe numbers up to near the level of iOS. . . for Android 4.4 users. 4.3 and below were given up as a lost cause, considering that their graphics processing is on the level of the iPhone 3.

Finally: swipe is working!

After a few days of performance tweaking, swipe was ready to go live. If you’re on a mobile device, head over to the zoosk.com mobile site to check it out!