Recently at Google I/O, I presented some techniques for writing smarter animations in your Android applications, specifically for making animations play nicely with reactive architectures:
I know that watching a 32 minute video isn’t everyone’s cup of tea, so here is a write up of the subject. ☕️
I think that animations are important for the usability of your app; they explain state changes or transitions, establish a spatial model or can direct attention. They help users understand and navigate our apps.
This example shows the same flow in an app — with animations enabled on the left & disabled on the right. Without animations, the experience feels abrupt; jumping between states without explaining what has changed.
So while I think animations are important, I also think that they are becoming harder due to changes in how we architect modern apps. We’re moving most state management out of the view layer to some controller (like a
ViewModel), which then publishes some kind of state object e.g. a
UiModel which encapsulates the current state of the app needed to render the view. Whenever something changes in our data model e.g. a network request returns or a user initiated action completes we then publish a new UI Model, encapsulating the entire updated state.
I don’t want to focus on this pattern today or its benefits. There are many good resources on this: look for Uni-directional Data Flow or MVI or libraries like MvRx or Mobius. But I do want to focus on the other end of this stream: where the view observes this stream of models and binds them to the UI. This is like a pure function, where given a new state, we want to bind it completely to our UI. We don’t want to think about the current state of the UI. That is, the binding of the data to your UI should be stateless. Animations however are stateful. They are all about moving from one value to another over time. This is the essential tension I want to focus on in this post. Because right now I fear that many apps are removing animations because of this tension, leading to a real loss of usability.
…binding data to your UI wants to be stateless. Animations however are stateful.
What’s the problem?
To look concretely at how we might retain animations in this reactive world and the challenges we need to address, here’s a minimal example: a login screen.
When the user hits login we want to hide the login button and show a progress indicator but we want to fade out the login button and fade in the progress indicator.
A state object for this screen and (static) binding logic might look something like this:
So if we want to animate this change, an initial attempt might look something like this where we animate the alpha property (and finally set the visibility value in the case of fading out):
This can however lead to unexpected results:
Here a new UI Model is being published on every key press, but you can see that the progress indicator keeps showing up unexpectedly! Or if you hit the submit button (with exaggerated animation durations for the demo) we can end up in a bad state where both the button and the progress indicator are gone. This is because our animations have side-effects, such as the end listener, which aren’t being handled correctly.
When writing animations in this reactive world there are a few qualities that your animation code needs. I’d categorize them as being:
Reentrancy means that your animation needs to be prepared to be interrupted and called again at any time. If new state objects can be published at any time then any animations that we run need to be prepared for a new state to be bound while an animation is running. To do this we need to be able to cancel or re-target any running animations and clean up any side-effects (like listeners).
Continuity is about avoiding abrupt changes in the value being animated. To demonstrate this property, consider a view which animates its scale & color when pressed/released:
All looks fine when we run the animation to completion but if instead we tap rapidly, we see the animation jump in size and color. This is a consequence of making assumptions in our binding code e.g. assuming that a fade animation always starts from 0 alpha.
To understand this property, consider this example where a view animates to the top left or top right in response to an event:
If we send it to the top right twice in quick succession, then the view stops mid-way before slowly continuing to its destination. If we change destination mid-flight then again it stops and abruptly changes direction. These kinds of sudden stops or changes of direction appear unnatural — nothing in the real world acts like this. We should aim to avoid these kinds of behavior, to keep our animations smooth.
So let’s return to our visibility binding function and fix these issues. Firstly, let’s look at continuity. We can see that our alpha animations always run from an initial to a final value e.g. 0 to 1 to fade in. We can instead omit the initial value and only provide a final value.
If you omit the initial value, then the animator will read the current value and start from there. This is exactly what we want and will avoid any sudden jumps in the property being animated.
Now let’s make our function reentrant; where it’s safe to call again at any time. Firstly we can be lazy and avoid doing any work that we don’t need to. If the view is already at the target value then we can return early.
Next we need to store the running animators and listeners so that we can cancel them before starting a new animation. The logical place to store this is in the view itself… but
View already offers a handy mechanism for doing this:
ViewPropertyAnimator. This is the object returned by calls to
View.animate() and it automatically cancels any currently running animations on a property if you start a new one — awesome!
ViewPropertyAnimator also offers a
withEndAction method which only runs if the animation runs to completion normally, not if it is cancelled. This is again exactly the behavior that we want, meaning that any side effects (like our visibility change) won’t run if the animation is cancelled by a new target value coming in. Switching to a
ViewPropertyAnimator makes our function reentrant.
We said that
ViewPropertyAnimator would cancel any animation running over the same property and start a new one. This violates our smoothness property and can lead to the stuttering problem we saw before where one animation abruptly stops and another begins (with the same duration, despite it being a shorter distance). To address this we can look at an animation library that I don’t think many developers are familiar with.
Springs are part of the ‘dynamic-animation’ Jetpack library. I think many people might have skipped this library when they see examples of very bouncy animations — while this effect can be useful, it’s not always needed or desirable. This bounciness however, can be disabled, leaving us with an animation system backed by a physics model which has a number of properties that are useful for general animation; specifically interruptibility and re-targeting.
So going back to our previous example, if we re-implement this with spring animations, we can see that it does not suffer from the smoothness issues. Instead it handles changing the destination and repeated starts, respecting the current velocity to yield smooth animations:
SpringAnimation looks a lot like a regular
Animator; much of the benefit comes from using the
animateToFinalPosition method rather than calling
start(). This will start an animation if it isn’t started yet, but crucially if there is an animation underway, it will retarget it to a new destination, maintaining momentum rather than changing abruptly.
Unfortunately there isn’t a convenient
View API like
View.animate to use springs (it’s Jetpack only)… but we can build one as an extension function:
This creates or retrieves a spring for a given
ViewProperty (translation, rotation etc), storing it in the view’s tag. We can then easily use the
animateToFinalPosition method to update a running animation. Using this in our visibility binding function:
We also need to switch the end action to use a spring animation end listener. You can find the full code for this in this gist. You’d also likely want to be able to configure the animation somewhat; unlike regular animations which specify durations and interpolators, springs can be configured by setting their stiffness and damping ratio. We can enhance our extension function to accept parameters for this making it easy to configure are the call-site, but offering sensible defaults. See here for a more complete implementation.
So we’ve made our visibility binding reentrant, continuous and smooth. While it might seem involved to achieve this, in reality you will only require a few of these binding functions which can then be used throughout your application. Here’s a library that has packaged up this spring technique for easy use.
Let’s take a look at another example of using this kind of animation; applying these principles to a
This example simulates updates happening to the data set while animations are running using the shuffle button. Notice when we hit the button twice in rapid succession that the smoothness of the spring based animator makes a big difference. On the left, the box stalls then changes direction. On the right it smoothly changes direction. I bet most apps load information from the network and display it in a
RecyclerView, likely from multiple sources. This kind of flexible animation adds a level of polish to your application that makes the experience much smoother. Here’s a PR adding this kind of animator to the Plaid sample.
Hopefully, the principles I’ve laid out in this post will help you to write animations in your reactive apps, improving their usability. Really they are an ordered list:
Being reentrant is about correctness, if you don’t have this property then your animations might be broken. Use
ViewPropertyAnimator or be careful in your animation code to handle the situation that it might be interrupted and called again.
Continuity helps improve the user experience, avoiding abrupt changes or jumps. It’s about avoiding assumptions in your animation code and ensuring an easy hand-off between animations.
Smoothness is the icing on the cake 🎂. It makes your animations feel more natural and enables dynamic changes, interruptions and re-targeting.
I truly believe that animations not only make our apps more delightful and enjoyable to use, but easier to understand. I encourage you to learn these techniques so that you can successfully employ them in your apps.