Recently I built a proof of concept HTML5 cross-platform Mobile App, which we intended to use as a technology showcase running on both iPhones and Android phones. I used animated transitions to give the user good feedback when moving between screens and interacting with the App. For example, screens would slide in from the left, and “Hero” elements would move to new positions and change shape/style.
The App looked great running on iPhones and the Android devices we had available during development (Nexus 5 and 10). However, some early testers tried it on other Android phones and reported that the transitions were jerky and didn’t look good.
Google’s success at making the OS available at lower price points means that many users have devices with relatively limited performance. Giving those users a good experience is vital. Here are some lessons I’ve learned while optimising my App to look good on older or slower devices.
About the App
My proof of concept App consists of a home screen showing a scrollable list of cards. Each card shows some summary information, and clicking on the card initiates a transition to the details screen. Some of the the card content also appears in the header of the details screen, so it uses a “Hero” transition to move it to its new position. This diagram illustrates the transition:
Cache or pre-fetch data
The data for the card details page comes from an Ajax request, so a simple implementation would look something like this:
The Ajax request is asynchronous, and subject to internet latency, so the screen transition is likely to be half-way through when the data comes back and displays the details screen. Yuck!
As it happens, some of the data on the details screen was used to create a summarised version on the home screen. Instead of requesting it all again, let’s cache and re-use it:
Now the important parts of the details screen will be displayed before the transition begins. We’ll still get some additional details popping in mid-transition, but I’ll have a look at that problem in a later paragraph: Avoid DOM updates during transitions.
Use CSS transitions
Rather than try to explain how Angular Animations work, I’ll refer to the documentation. I created an
animation module, with
leave methods for the screens containing the “Hero” elements.
My initial attempt at animating the “Hero” element used jQuery, something like this:
However, a CSS transition should be smoother, because it can be handled natively by the browser. Some phones even allow for hardware acceleration of CSS transitions.
In this example, the
animating class tells the browser to animate the
top attribute from
-webkit-transition: all ease-in-out 300ms;
-moz-transition: all ease-in-out 300ms;
-o-transition: all ease-in-out 300ms;
transition: all ease-in-out 300ms
// Final appearance of the animated element
The CSS transition also lets us animate other properties, such as the foreground and background colours. These are different on the details page because the element moves inside the header.
Selectively disable transitions
The CSS transitions look nice and smooth on the higher-end devices, such as the Nexus 5 and Nexus 10, but older devices still struggle. I tested with a HTC Desire S, which could handle the simple side-to-side transition of the page, but not animating the “Hero” element.
A good way to solve this is to intelligently scale back the experience, depending on the hardware capabilities. I want the “Hero” animation on the Nexus 5, but on the Desire S it should stick to the basic side-to-side transition.
Ionic framework already has a built in feature for just this sort of situation. The
ionic.platform object has a property
grade which is ‘a’ for newer/faster hardware, then ‘b’ or ‘c’ as things get slower. I can limit “Hero” animations to grade ‘a’ devices like this:
Avoid DOM updates during transitions
$scope, which trigger DOM updates. The updates may not take long, but it can be enough to delay one of the frames of the animation, which creates a noticeable and unsightly judder.
My first attempt to avoid callbacks during transitions was to simply delay the request until after the transition had finished. Like this:
In practice, this was difficult to manage because data requests are made in many different places, and it makes the code harder to read and maintain. It’s also unreliable for a couple of reasons. Firstly, it’s hard to make sure there are no outstanding timeouts or ajax calls when a transition begins. Secondly, sometimes it takes longer for Angular to get the transition started so the timeout ends up firing mid-transition anyway.
In the end I settled on a system of guarded callbacks. The Ajax requests are made in the normal way, but the callbacks are delayed if there is an in-progress transition. I implemented this using an Angular service:
callbackService is injected into the animation class, which calls
endAnimation when the transition starts and finishes respectively.
dataService class also gets
callbackService injected, and uses it to wrap callbacks from
$http requests, like this:
The optimisations I’ve tried have noticeably improved the User Experience on high performance devices like the Nexus 5, which has fewer dropped animation frames and feels smoother and more responsive. More importantly, it has made a dramatic improvement on the lower end devices. Where the transition was previously slow and jerky, it is now a simple slide-left with nothing else competing for CPU time.
As a developer, it’s easy to focus on high-end devices like iPhones and premium Android handsets, but providing a good user experience for all Android users requires a little optimisation and lots of testing.