AnimatedStateListDrawable / Animation / BottomNavigationView / VectorDrawable

BottomNavigationView: Animating Icons

There is an occasional series on Styling Android which covers techniques for creating Animated Icons as AnimatedVectorDrawable and AnimatedStateListDrawable. These are quite easy to use when using them inside a standard ImageView, but there can be other scenarios where they can be a little trickier to get working, and one of those is in the BottomNavigationView control from Material Components. In this article we’ll look at some tricks for getting them to play nicely.

There are two distinct types of animated icons that we’ve covered in the Animated Icons series: Those that animate based on changing view state (such as the strikethru animation on the left), and those that are ongoing repeating animations such as the LoadingV2 animation. The first are achieved by using an AnimatedStateListDrawable, and the second are implemented using AnimatedVectorDrawable and need to be started and stopped manually (as seen here).

This is easy enough to implement when using these Drawables within standard ImageView, but they do not work quite as expected when using them within BottomNavigationView. I am indebted to my colleague at Babylon Health Maxime Mazzone from whom I learned of this issue – you have him to thank for the investigations which prompted this article.

The problem is not with the view state-based animations – using AnimatedStateListDrawable for the icon drawable in MenuItems will animate according to the view states triggered by user interaction with the BottomNavigationView:



    
    .
    .
    .

In this case we use the exact same strikethru drawable from this article. The problem arises when we want to use an on-going repeating icon such as the loadingv2 drawable from this article:



    

    
    .
    .
    .

We’ve already explained how using an AnimatedVectorDrawable requires us to manually start and stop the animation, so it would seem that all we need to do is something like this:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

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

        // Don't try this at home - it doesn't work!
        binding.bottomNavBar.menu.startAnimations()
    }

    private fun Menu.startAnimations() {
        (0 until size()).map { get(it).icon }
            .filterIsInstance()
            .forEach { it.start() }
    }
}

What this is doing is going through each of the menu items, and for any where the icon drawable implements the Animatable2 interface, we call start() to kick off the animation. This code works as it should, and finds an AnimatedVectorDrawable (which implements Animatable2) for the relevant MenuItem, and start() is called on that drawable.

Internally Drawable has a Callback to the parent view which the Drawable can call to invalidate itself, and this mechanism is used by the start() method of AnimatedVectorDrawable to invalidate the parent View to trigger a re-draw during which the animation will be started. For some reason – I’m not sure whether it is by design because it interferes with the operation of BottomNavigationViewBottom=NavigationView does not appear to register a Callback with the Drawable so the redraw never gets triggered, and therefore the animation does not start.

There are a few ways to work around this depending on the exact use-case we’re aiming for. The first is where we just want the animation to run continuously. The trick is to hook the animation starting in to the view state by wrapping the AnimatedVectorDrawable inside an AnimatedStateListDrawable:



    

    

    


This uses the AnimatedVectorDrawable that we used earlier, but adds a transition between states to start it. As the animator it uses internally is a repeating one, then it starts a repeating animation. The actual view state transition is when the MenuItem itself, or the parent BottomNavigationView receives focus. This happens automatically in my layout as the BottomNavigationView automatically receives input focus, but if this doesn’t work in more complex layouts you may need to do something funky once the layout has been inflated to force focus to the BottomNavigationView in order to trigger the animation, then restore it back to whatever is supposed to have initial focus shortly after.

We can now use this AnimatedStateListDrawable as the icon within the menu:



    

    
    .
    .
    .

This now has an ongoing animation which continues even through selection state transitions:

The next use-case we’ll look at is where we only want the animation to run when the menu item is active and stop when it is inactive. Once again, we need to do this using an AnimatedStateListDrawable but in this case we’ll transition between static and animating states depending on checked state:



    

    

    

    


In checked state we’ll use the AnimatedVectorDrawable that we saw earlier, and in unchecked state we’ll use the static VectorDrawable that is used within the animated version. This now behaves like this:

The static version of the graphic has a single box showing – this is so that we can see that there is no animation taking place – in a real scenario you would want something a little more meaningful. Note how the animation is interrupted immediately when the MenuItem loses checked state.

The final use-case we’ll consider is where we want to change the animation state depending on some external state. For example we may want to animate the icon when something else is loading, and stop the animation once the loading is completing. For this example we’ll add a switch to the main layout, and the changing state of this switch will control when the icon for a specific MenuItem is animated or static.



    

    


We now add an OnCheckChangedListener to this switch which changes the drawable used for a specific MenuItem:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

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

        binding.toggle.setOnCheckedChangeListener { _, isChecked ->
            binding.bottomNavBar.menu.findItem(R.id.menu_loadingv2_external).icon =
                if (isChecked) {
                    resources.getDrawable(R.drawable.loadingv2_ongoing_selector, theme)
                } else {
                    resources.getDrawable(R.drawable.loadingv2_vector, theme)
                }
        }
    }
}

The important thing to note is that for the checked state we use R.drawable.loadingv2_ongoing_selector which is the AnimdatedStateListDrawable that we used earlier for the ongoing animation – this will get started automatically. For the non checked state we use the static VectorDrawable the same as the previous use-case:

When the switch is on, the MenuItem icon has a repeating animation which behaves independently of the checked state of the MenuItem itself. If you wanted something that only animates when both the switch is on, and the MenuItem has checked state, then you would use R.drawable.loadingv2_stateful_selector instead of R.drawable.loadingv2_ongoing_selector.

Of course, this external control technique isn’t just for repeating animations – it is possible to trigger simple state transitions by using different AnimatedVectorDrawables inside the AnimatedStateListDrawable that we use in the OnCheckChangedListener.

While there are some problems if you try and use AnimatedVectorDrawable directly within BottomNavigationView these can easily be overcome by wrapping this inside an AnimatedStateListDrawable and we can get some pretty fine-grained control over how and when our animation run with some creative application of this.

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.