Re-animation

In a previous article, I described a technique for creating vector animations on Android:

At the time of writing, part of this technique (path morphing) was only supported on Lollipop and newer versions of the OS. Version 25.4 of the Android Support Library however, back-ported this capability all the way back to Ice Cream Sandwich (i.e. an impressive 99% of devices). Check out this Google I/O session covering this and other awesome changes in the Support Library:

I wanted to try out these new capabilities by updating my example to work on older devices and share my findings.

14 is the new 21

The first step was to simply change my minSdkVersion from 21 to 14. Opening up the exact same AnimatedVectorDrawable, the Lint tool pointed out a couple of errors. Specifically I was using some of the standard Material interpolators which were only introduced in API 21 so wouldn’t be available on older platforms. As the Support Library also back-ports PathInterpolators I simply copied their implementations from the platform into my project and referenced these instead.

I also set vectorDrawables.useSupportLibrary = true in my build.gradle file to tell the build toolchain not to strip away vector resources on older devices.

Under construction

To play the animation, you need to get a reference to it in code and call start(). There are multiple ways that you can actually create the animated drawable:

  1. Reference it in your layout (app:srcCompat="@drawable/avd_foo") and later retrieve the drawable from the ImageView.
  2. Use AppCompatResources#getDrawable
  3. Use AnimatedVectorDrawableCompat#create

I found that methods 1 & 2 can return different concrete classes; either an AnimatedVectorDrawable or an AnimatedVectorDrawableCompat depending upon which API you find yourself on.

Interestingly the support library currently uses the native version on API 24+ and the compat version prior despite the class being introduced in API 21. This enables it to supply bug fixes to APIs 21–23.

This can be problematic if/when you need to cast the drawable.

Note that both classes implement Animatable so if all you need is to start/stop it then you can cast away safely. Additionally AnimatedVectorDrawableCompat offers a handy static method to register callbacks, which will check which type we’re dealing with and delegate the callback as appropriate.

Instead I opted for door number 3; always using the compat class. This might add a tiny bit of overhead (as on newer platforms AVDC just delegates everything to the native class) but it made my consuming code simpler.

Call me back

One wrinkle I found was with the technique I used to make the animation loop. Unfortunately AnimatorSets do not support repeating, so I worked around this by adding an AnimationCallback which listens for the end of the animation and calls start again. This did not work on older platforms but I was able to work around it by posting the start call on a handler to be executed after the end callback:

avd?.registerAnimationCallback(
object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
imageView.post { avd.start() }
}
})

Stale state

Parts of the animation only run at certain points within the loop; for example the dots fade in/out when they enter/exit the scene. On older devices I found that their ‘state’ wasn’t being reset (to how it was defined in the VectorDrawable) on each loop.

Notice how the grey dots are (incorrectly) visible when they enter from the right, then briefly disappear and then fade in. To fix this I added a zero length animation to set properties to their expected value at the start of each loop so that they’re ready to be animated e.g.:

<!-- Fade dot 5 in as it enters the scene -->
<set>
<objectAnimator
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="0"
android:duration="0" />
<objectAnimator
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:startOffset="1900"
android:duration="60"
android:interpolator="@android:interpolator/linear" />
</set>

Form a queue

The last issue I hit was a problem with sequentially ordered AnimatorSets. The main pin jump animation is a sequence of path morph animations, from keyframe to keyframe. My animation assumed that this sequence would run for the sum of all of the individual animator’s durations. On older platforms however, a bug causes each animator to wait for the next frame boundary before starting. These small delays add up such that the animation took longer than the sum of durations, so other parts of the composition would be mis-timed. I was able to work around this by switching to ordering="together" instead and using startOffsets on each individual animator to start them at the right time.

Impressively unimpressive

The end result is extremely impressive in it’s un-impressiveness. That is, the animation looks exactly the same as before but now runs on many more devices.

Animation running on API 16. #holoyolo.

Even though I did encounter some issues getting this to work on older devices, they were all pretty easily resolved and I think that the ability to run on so many more API levels makes the effort well worth it.

I was pleased with how many things just worked including the XML bundle format which allows you to specify the VectorDrawable and animations in a single file. Lint tooling was also helpful in pointing out some problems. You can find my code for the back-ported animation here on Github.

If you were holding off adding awesome animations to your application because of lack of API support, then hold-off-no-more. If you’re looking into path-morphing animations then be sure to check out Alex Lockwood’s amazing ShapeShifter tool which will help you to create morph-able shapes. If this has inspired you to create something, then let me know!

Show your support

Clapping shows how much you appreciated Nick Butcher’s story.