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 explaining 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 API:
Notice the abrupt change in velocity when the target changes. When the user moves their 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:
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 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 shopreme.
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();
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 ofViewPropertyAnimator
). - Moving a view along a path (optionally, while rotating tangentially).
A more complex example
Here’s a fairly complex animation involving multiple views and delays between the animations:
Here’s the code it takes to create this animation:
AdditiveAnimator.animate(views, 50L).x(x).y(y).rotation(r).start();
We simply pass a list of views to the animate method, and an optional stagger
parameter (50 milliseconds in this case) which specifies how much of a delay there will be between applying the following animations to the list of views.
Here’s another, more verbose, way of building this animation that more explicitly shows what is going on under the hood of the multi-view staggered animation:
AdditiveAnimator anim = new AdditiveAnimator();
for (View v : views) {
anim = anim.target(v).x(x).y(y).rotation(r).thenWithDelay(50);
}
anim.start();
target(View v)
changes the current animation target to the given view. There’s no need to use AnimatorSet
when animating multiple views!
This method is very handy when you want to animate different properties for each view. (There’s also a targets()
method which takes a list of targets — useful in cases where you want the same animation to be run on multiple views.)
The final method call in the builder — thenWithDelay(int millis)
is one of my favourite features of AdditiveAnimator
and deserves its own section (see Animation Chaining).
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);
final Animator alphaAnimator = ObjectAnimator
.ofFloat(view, View.ALPHA, 1f, 0f);
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(
translationAnimator,
alphaAnimator
);
animatorSet.start();
Here is the same animation built with AdditiveAnimator
:
AdditiveAnimator.animate(view)
.translationY(-100)
.then()
.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 without affecting the previously created animations. For example, you could set a new duration
for each chained animation.
There are more flavors of then()
, all of which are more than inconvenient to create using the standard animation APIs:
thenAfterDelay(int millis)
waitsmillis
milliseconds after starting the previous animator before executing the next animation. (“Run 400ms after the previous animation starts”)thenBeforeEnd(int millis)
lets you specify a negative offset to the end of the previous animation. (“Run 400ms before the previous animation ends”)thenAfterEnd(int millis)
lets you specify an offset after the end of the previous animation. (“Run 400ms after the previous animation ends”)
Here’s a visual comparison of all of those functions when called with the same millis
value:
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 definedbounceTwice()
in yourMyAnimator
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 aTextView
:
FloatProperty<TextView> textColorProperty = FloatProperty.create(
"textColor",
tv -> tv.getTextColor(),
(tv, color) -> tv.setTextColor((int) color)
);AdditiveAnimator.animate(myTextView)
.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()
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). - is highly profiled and optimized for performance. All allocations are made at animation build-time so that there are no GC-related hiccups while the animation is running.
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 newest version of the library by adding
dependencies {
compile ‘at.wirecube:additive_animations:1.9.3’
}
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!