Animation / Lottie

Lottie: Delayed Loop

Lottie is a very nice library which enables vector animations that are more complex than can be done using Animated Vector Drawables. The main use-case for Lottie is that it takes animations that are generated using Adobe After Effects (exported using the Bodymovin plugin). After Effects is a tool aimed at designers rather than developers, so is a good enabler for getting animations created by designers in to our apps. The APIs are actually quite straightforward, so I won’t be giving a tutorial here, for those that are unfamiliar with Lottie I would suggest reading the official documentation. For this post we’ll cover a somewhat more complex use-case that is actually surprisingly easy to get working.

The animation that we’re going to work with is the one on the left which I obtained under Creative Commons license from lottiefiles.com. The important thing about this animation is that there are two distinct phases – there is an initial “loading” phase where the colours are orange, followed by a second “welcome” phase where the colours are blue. In my day job I recently had a double phase animation such as this, but the requirement was that the animation played from the start initially, but rather than repeating indefinitely from the start, only the second phase should repeat.

The initial thought was to ask the designer to split this in to two separate animations, and then run the first phase one, and once that completes to run the second on an infinite loop. While this would certainly work, I found a way of achieving this using a single animation.

Lottie supports the looping of a subsection by setting a start and end point for the loop. These values can be set using either a fraction of the animation (0.0 is the start and 1.0 is the end), or by specific frames – these will be specific to each animation. Specifying the loop range doesn’t do what’s required because it loops the subsection as soon as the animation is started – so it does not support the playing of the first phase, and then looping on the second phase. But we can get creative!

Let’s start by checking out the layout:



    

    

We have the LottieAnimationView and a button to restart the animation manually. We’re setting the Lottie animation from a URL and setting it to loop, which by default will be an indefinite loop.

As this stands it will keep looping the entire animation which isn’t quite what we want. We define the behaviour is our Activity:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.restartButton.setOnClickListener {
            resetLottieAnimation()
        }

        resetLottieAnimation()
    }

    private fun resetLottieAnimation() {
        with(binding.lottieAnimation) {
            setMinAndMaxProgress(0f, 1f)
            frame = 0
            addAnimatorUpdateListener(UpdateListener(this))
            playAnimation()
        }
    }
    .
    .
    .
}

The resetLottieAnimation() method runs the animation from the start. It first sets the min and max points of the loop to 0f and 1f respectively, which specifies a loop on the entire animation; then it sets the current frame to 0 – which is the start; next it adds an AnimatorUpdateListener – we’ll look at this in more detail in a moment; and then plays the animation.

There is a potential gotcha here. It is important to reset the frame to frame 0 after setting the new max and min position for the loop. If you don’t setting the frame to 0 will not work if frame zero is outside the current loop – Lottie has some bounds checking which will prevent you from setting a frame position outside of the currently specified loop. But everything will work correctly if you set the frame position after resetting the loop to the entire animation.

This resetLottieAnimation() method is essentially providing the same behaviour as the layout – it will loop the entire animation. This is necessary for the restart button. So when we start the animation, either when the Activity is created, or when the restart button is clicked. We start the animation to loop over the entire animation, but add an AnimationUpdateListener and it is this that does the magic:

class MainActivity : AppCompatActivity() {
    .
    .
    .
    private class UpdateListener(private val lottieAnimationView: LottieAnimationView) :
        ValueAnimator.AnimatorUpdateListener {
        override fun onAnimationUpdate(animation: ValueAnimator?) {
            if (lottieAnimationView.frame > LOOP_START_FRAME) {
                lottieAnimationView.removeUpdateListener(this)
                lottieAnimationView.setMinAndMaxFrame(LOOP_START_FRAME, LOOP_END_FRAME)
            }
        }
    }

    companion object {
        private const val LOOP_START_FRAME = 39
        private const val LOOP_END_FRAME = 80
    }
}

The two constants, LOOP_START_FRAME and LOOP_END_FRAME are specific to this particular animation. I determined the values to use by viewing the animation on lottiefiles.com, the player allows us to scrub to specific points of the animation and displays the frame number that we’re currently viewing. I used this to determine the start and end frames of the second phase of the animation.

The onAnimationUpdate method gets called repeatedly as the animation plays, and so we check whether the current frame is greater than LOOP_START_FRAME. If not, we do nothing and the animation keeps playing. However, if the current frame is greater than LOOP_START_FRAME we know that the animation is currently within the second phase. We first unregister the AnimationUpdateListener as we no longer need to monitor the position of the animation; then set call setMinAndMaxFrame on the LottieAnimationView to set the loop bounds.

So initially the loop bounds are the entire animation, and this causes it to play from the start. Once we enter the second phase of the animation, then we update the loop bounds to the second phase. This results in the first phase playing through once, and then we indefinitely loop on the second phase (the gif restarts after the second phase has looped four times, but it actually loops until the restart button is pressed):

Adjusting the loop bounds mid way through the animation running for the first time has enabled us to get exactly the behaviour that we want without having to get the designer to split the animation in two. Although it may feel like we’ve had to add some logic to achieve this, we would still have to add a similar amount of logic with the animation split in two. First play the first phase, add a listener to detect it completing and then play the second phase on infinite loop. This feels slightly cleaner, to me.

The source code for this article is available here.

© 2020, Mark Allison. All rights reserved.

Copyright © 2020 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.