Animation / Kyrie / MotionLayout / VectorDrawable

Motion Vectors

Regular readers of Styling Android will not be surprised to hear that I rather like animations. A couple of really useful tools for creating nice animations are AnimatedVectorDrawable (AVD), and MotionLayout and it should come as no surprise that I am a big fan of both. But using them together is not easy. In this article we’ll look at why that is, and find a nifty trick to get them to collaborate with provides some great scope for creating really nice animations in apps.

Let’s begin by understanding the problem that we’re trying to solve here. The animation on the left is one that we previously looked at the mechanics of in the series on Animation Icons. It depicts the transition between an expanded and collapsed state. If we were to position this within a Collapsing Toolbar it would be really nice if we were able to transition between these states as the MotionLayout transitions between the collapsed and expanded states of the Toolbar.

We’ve covered how to create a Collapsing Toolbar using MotionLayout before, so it might appear that we just need to combine the outputs of these two previous posts to get what we want. Unfortunately it is not as simple as that may sound.

The basic problem that we have when is that AVD is designed to run either as a repeating animation, or a fixed duration one. It is not designed to be controlled by external agents and, in this case, MotionLayout is the external agent that we want to use to drive the animation. More specifically, MotionLayout allows us to animate properties of its child Views, and we if we could expose the ability to set the playback position of the AVD through a custom subclass of AppCompatImageView then all would be golden. Unfortunately AVD does not provide an API to set the playback position. It allows us to start, stop, and pause the animation, but not explicitly set the playback position.

So it looks like we either have to give this up as something we cannot achieve, or look for a hidden / private API that might allow us to do what we need. The latter is a bad idea because these are liable to change or even be removed without notice, and Google is really trying to disabuse developers of this practise.

There is actually a third option, and that is to use a third-party library for our AVDs. Alex Lockwood is a Vector Guru. He’s produced some great tools for working with vectors such as ShapeShifter. He has also created a library named Kyrie which provides the same functionality as VectorDrawable and AnimatedVectorDrawable, but with an augmented API surface, as well as a programmatic way of creating them (which itself opens up some amazing possibilities). The enabler for this post is that it provides an API for setting the playback position programmatically.

We start by including kyrie as a dependency in our app/build.gradle:

.
.
.
dependencies {
    .
    .
    .
    implementation "com.github.alexjlockwood:kyrie:0.2.1"
}
.
.
.

It is important to know that Kyrie does not require us to implement the support library versions of AVD and VD – it’s not a wrapper around them, and has no dependency on them. If you also require the support library implementations in some places within your app, you may need to use them where appropriate, and Kyrie where necessary, which may increase the size of your APK slightly. That said, Kyrie is a really small library which will not provide significant bloat.

With Kyrie added to the project we now need to wire it up. The first thing we need to do is subclass AppCompatImageView:

class SeekableImageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    private var kyrieDrawable: KyrieDrawable? = null

    init {
        if (drawable is AnimatedVectorDrawable) {
            @Suppress("CustomViewStyleable")
            context.obtainStyledAttributes(
                attrs,
                R.styleable.AppCompatImageView,
                defStyleAttr,
                0
            ).apply {
                setImageResource(
                    getResourceId(R.styleable.AppCompatImageView_srcCompat, -1)
                )
                recycle()
            }
        }
    }

    override fun setImageResource(@DrawableRes resId: Int) {
        kyrieDrawable = KyrieDrawable.create(context, resId)?.also { drawable ->
            setImageDrawable(drawable)
        }
    }

    /**
     * Scrubs to a specific point if the drawable is an AVD.
     */
    @FloatRange(from = 0.0, to = 1.0)
    var seek: Float = 0f
        set(value) {
            field = kyrieDrawable?.run {
                currentPlayTime = (totalDuration.toFloat() * value).toLong()
                value
            } ?: 0f
        }
}

There’s a small hack that we need to do here in the init method which will be called after AppCompatImageView has parsed the View attributes. We cannot intercept this, and Kyrie does not support creating a KyrieDrawable from an existing Drawable. So we have to check whether AppCompatImageView has already inflated an AnimatedVectorDrawable and, if so, create a KyrieDrawable in its place. This does mean that sometimes we’ll have to inflate the AVD twice, but that will only ever be done during layout inflation. Any subsequent calls to setImageResource() will immediately attempt to create a KyrieDrawable. One thing worth noting here: If a resource ID for a Bitmap is passed in here we’re going to crash. So for production code some checking here would be wise.

The seek field is the key bit that we need – it allows the animation playback position to be set with a value between 0.0 (the start of the animation) and 1.0 (the end of the animation). All of the logic for interacting with the KyrieDrawable is done here, and we expose a very generic 0.01.0 range to set the playback position.

I won’t bother explaining the entire MotionLayout that we’re using here as there’s little different from the previous CollapsingToolbar post. The key bit is in the MotionScene where we animate the seek property of SeekableImageView:



    
        
    

    
        .
        .
        .
        
            
        
        .
        .
        .
    

    
        .
        .
        .
        
            
        
        .
        .
        .
    

This animates the value of the seek field as part of the MotionLayout animation.

If we run this we get this:

I was chatting with Sebastiano Poggi about this subject, and he pointed out that we can already use Lottie animations with MotionLayout out of the box by animating LottieAnimationView.setProgress in exactly the same way as we’re animating SeekableImageView.seek in this example. Documenting that would have resulted in a very short post, so I have opted to cover a slightly trickier use-case!

So while using AVD within MotionLayout animations may seem quite tricky, with just a little bit of wrapping, Kyrie enables us to do precisely what we need, and there are a huge number of possibilities that this opens up.

The source code for this article is available here.

© 2019, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.