ga('send', 'pageview');

Animated Selector in Jetpack Compose

Are you migrating to Compose but can’t get your AnimatedStateListDrawables to work? Here’s a solution that worked pretty well for me 👍

Some background…

While trying to migrate BottomNavigationView to the Compose NavigationBar (Material3) I got stuck on errors when loading my icons in Compose. In my app, I’ve created AnimatedVectorDrawables using AnimatedStateListDrawable to automatically animate the navigation bar tab icons. I spent some time trying to find out how to use these in Compose, but AFAICT animated-selector is not supported in Compose 🥺

Original Material tabs with BottomNavigationView

It seemed I had to decide if I wanted to skip animations or make new ones in Compose. But instead, I postponed my decision so I could maybe come up with another solution later on.

After a few weeks, I was getting close to completing my Compose migration, but the BottomNavigationView was still a blocker. I like the fact that these animations/transitions are kept in resources and spending time to make new animations in Compose didn’t appeal to me since I’ve already spent time creating these animations in the first place. But it turns out that there was a 3:rd alternative which was pretty obvious: AndroidView 😀

By putting an ImageView inside an AndroidView I was able to load the animated-selector without any problem. But, putting these icons in NavigationBarItem still didn’t show any animation. The BottomNavigationView uses the Checkable interface to show which tab is active and the animated-selector transitions are set up using the checked state.

<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="">

android:state_checked="true" />

android:state_checked="false" />

android:toId="@id/checked" />

android:toId="@id/unchecked" />

So I made the ImageView checkable, and passed in the selected tab state to each AndroidView and here is the result:

Material 3 NavigationBar with AnimatedStateListDrawables

Show me the code! 😅

fun AnimatedIcon(
@DrawableRes animatedIcon: Int,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
modifier = modifier.size(24.dp),
factory = { context ->
CheckableImageView(context).apply {
val drawable = ContextCompat.getDrawable(context, animatedIcon)
isChecked = isSelected
if (drawable is Animatable) drawable.start()
update = { view ->
view.isChecked = isSelected

private class CheckableImageView(context: Context, attrs: AttributeSet? = null) :
AppCompatImageView(context, attrs),
Checkable {

private var mChecked = false

override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (isChecked) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET)
return drawableState

override fun toggle() {
isChecked = !mChecked

override fun isChecked(): Boolean = mChecked

override fun setChecked(checked: Boolean) {
if (mChecked != checked) {
mChecked = checked

companion object {
private val CHECKED_STATE_SET = intArrayOf(
private fun BottomNavigationTabs(
navController: NavController,
onSetTab: (BottomNavItem) -> Unit,
) {
val items = listOf(
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
val isSelected = currentRoute == item.route
val title = stringResource(id = item.titleRes)

selected = isSelected,
onClick = { onSetTab(item) },
alwaysShowLabel = false,
label = {
text = title,
style = MaterialTheme.typography.labelSmall,
icon = {
isSelected = isSelected,


Sure this feels is a bit hacky, but I’m happy I can still use AnimatedVectorDrawables, and I have a working NavigationBar. If there is a better (more Compose-ish) way I can use my animated-selectors as-is, I’d like to know.

Thank you for reading!

You can also read the original blog here and find more blogs from Peter here

Animated Selectable Item with Jetpack Compose | Android Studio Tutorial

By Peter Törnhult

Android utvecklare, Techcoach, Scrum master

Leave a Reply

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