Thanks to Raphael Loder for the image!

Bringing smooth animation transitions to Android

Why I wrote the AdditiveAnimation Android library and how it drastically improved the feel of our apps without any significant code changes — simultaneously making all animation-related code shorter, more performant and easier to read.


Before diving into details of what additive animations are, here’s a simple showcase to explain why you should use them. 
In this sample app (which I totally stole from here), the user taps or drags on the screen to trigger an animation that moves the yellow view towards the finger.

Here’s the normal version using the standard Android animation system (all gifs are downsampled to 30fps):

What changing the animation target usually looks like in Android — ugh!

Notice the abrupt change in velocity when the target changes. When the user glides his finger across the screen, the view doesn’t move at all — the animation is restarted before it has a chance to run!

Here’s the same interaction using additive animations:

Changing animation target with the AdditiveAnimator library.

Additive Animations

In our indoor location application, we periodically recalculate the current position of the user (think of the blue dot indicating your position in Google Maps). Whenever a new position is calculated, we animate the blue dot to the new position — but in Android, this cancels the currently running animation and starts a new one, resulting in disjointed movement! 
Having implemented this feature on iOS without any problems (because every UIView-based animation is performed additively since iOS 8), I investigated how to fix the discontinuity in momentum and found a concise and spot-on summary on this topic:

TL;DR: The currently running animation isn’t removed when adding another animation for the same property — instead, multiple animations can contribute to the animated value simultaneously. This has minimal impact on performance and can greatly improve the feel of an app — as it did for us in all of the Android apps we develop at wirecube.

The API

While Google has just announced a new physics-based animation API that can be used to create a similar effect, their new API requires significant code changes and is cumbersome and unintuitive to use in most situations.

In contrast, let’s look at the code of the previous example:

AdditiveAnimator.animate(view).setDuration(1000)
.x(touch.getX())
.y(touch.getY())
.start();

That’s it. If this seems familiar, it’s because I deliberately stuck close to the ViewPropertyAnimator API to make conversion as simple as possible.

Animations are supported out of the box for a lot of attributes:

  • Everything that ViewPropertyAnimator supports ( x, y, z, translationX, translationY, translationZ, rotation, alpha etc).
  • Margins, padding, size, elevation and scroll position of views with appropriate LayoutProperties.
  • Background color of views with a ColorDrawable background.
  • Delta values for (almost) everything (the *By() methods of ViewPropertyAnimator).
  • Moving a view along a path (optionally, while rotating tangentially).

A more complex example

Here’s a fairly complex animation involving multiple views, hardware layers and delays:

It looks much better in 60fps (download video here).

Here’s the code it takes to create this animation:

AdditiveAnimator anim = new AdditiveAnimator().withLayer();
for(View v : views) {
anim = anim.target(v).x(x).y(y).rotation(r).thenWithDelay(50);
}
anim.start();

There’s a lot going on here, so let’s break it down step by step:

withLayer() enables hardware animations for all views you subsequently add to the animation. All views in this sample use alpha < 1 to show that performance isn’t affected. Even with hundreds of animations running at the same time, the animation runs smoothly thanks to withLayer() and the highly optimised core of AdditiveAnimator. In the example above, there are between 800 and 1200 animations running at all times (which is definitely overkill for most applications).

target(View v) changes the current animation target to the given view. There’s no need to use AnimatorSet when animating multiple views!

x(int x), y(int y), rotation(float degrees) are the methods that create animations for the current target, just like they do for ViewPropertyAnimator.

The last method call — thenWithDelay(int millis) is one of my favourite features of AdditiveAnimator and deserves its own section.

Animation Chaining

One of the most common use cases of AnimatorSet (besides running multiple animations simultaneously) is playing animations sequentially.

Essentially, what you would have to do is this (courtesy of StackOverflow):

Animator translationAnimator = ObjectAnimator
.ofFloat(view, View.TRANSLATION_Y, 0f, -100f)
.setDuration(700);

final Animator alphaAnimator = ObjectAnimator
.ofFloat(view, View.ALPHA, 1f, 0f)
.setDuration(300);

final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(
translationAnimator,
alphaAnimator
);
animatorSet.start();

Here is the same animation built with AdditiveAnimator:

AdditiveAnimator.animate(view).setDuration(700)
.translationY(-100)
.then()
.setDuration(300)
.alpha(0f)
.start();

The then() call in this example creates a new AdditiveAnimator and establishes a parent-child relationship with the previous one, copying all of its properties (such as the animation target, duration etc), and sets its internal startOffset to the total duration of all previous animators (in this case, 700 milliseconds). The new animator can then be configured and modified as needed.

There are more flavors of then(), all of which are more than inconvenient to create using the standard animation APIs:

  • thenAfterDelay(int millis) waits millis milliseconds after starting the previous animator before executing the next animation.
  • thenBeforeEnd(int millis) lets you specify a negative offset to the end of the previous animation.
  • thenAfterEnd(int millis) lets you specify an offset after the end of the previous animation.

Here’s a visual comparison of all of those functions when called with the same millis value:

From left to right: thenAfterDelay(400), thenBeforeEnd(400) and thenAfterEnd(400)

Stuff that didn’t make it into the post

There is simply too much to talk about for one blog post, so here’s a quick rundown of the most important features.

AdditiveAnimator…

  • was designed to be easily extensible for your specific use-case by subclassing. There are hooks for subclasses to apply custom property values that don’t easily map to any single property value and all relevant methods for composing and managing animations are exposed (but only the ones that don’t let you shoot yourself in the foot 🔫 😵).
    Let’s say you defined bounceTwice() in your MyAnimator class. AdditiveAnimator lets you use this method, no matter how you construct your animation:
new MyAnimator(view).x(100).then().bounceTwice().start();
  • can animate any custom property additively without subclassing simply by specifying a getter and setter for the property value and provides access to custom TypeEvaluators
    Here’s an example using these two features to animate the text color of a TextView:
TextView animatedView = ...;
FloatProperty textColorProperty = new FloatProperty("textColor") {
public Float get(View object) {
return Float.valueOf(textView.getCurrentTextColor());
}
public void set(View object, Float value) {
textView.setTextColor(value.intValue());
}
}
AdditiveAnimator.animate(textView)
.property(Color.RED, // target value
new ColorEvaluator(), // TypeEvaluator
textColorProperty)
.start();
  • works with multiple views simultaneously without having to use AnimatorSet or any other wrapper.
  • can animate along paths (including tangential rotation) — and since all animations are additive, you can even animate along multiple paths at the same time, creating an animation such as this.
  • supports animation chaining with elegant syntax (then(), thenAfterDelay(int millis) etc.).
  • allows you to switch the TimeInterpolator midway through creating an animation. This is particularly useful for when you want a spring animation for the position, but don’t want the bouncing to affect the alpha value:
AdditiveAnimator.animate(view)
.setInterpolator(new BounceInterpolator())
.x(100).y(200)
.switchInterpolator(new LinearInterpolator())
.alpha(0)
.start();
  • automatically handles shortest-distance rotation computation for you (never worry about computing the shortest angle again).
  • supports hardware layer animations using the familiar withLayer() syntax.
  • includes intuitive APIs for standard features like cancellation of all or just a few specific animations, adding multiple start/end/update listeners, setting a repeat mode/count and getting the current animation target value (or the latest queued value for animations that haven’t been started yet).
  • allows for global setup of default values to be used in all animations if no other value is specified (like the TimeInterpolator or animation duration).

Going into detail about all of these features is too much for a single blog post, so stay tuned for more code samples, demos and new feature announcements!

Getting the library

The library supports API Level 14+ (Android 4.0+). The source code, complete with a demo project containing all referenced samples and more, is available on GitHub, and you can grab the library for your Android project by adding

dependencies {
compile 'at.wirecube:additive_animations:1.3'
}
...
repositories {
jcenter()
}

to your build.gradle file.

If you have feature requests or suggestions for improvements, feel free to open a ticket on GitHub or leave a comment!