Are you migrating to Compose but can’t get your AnimatedStateListDrawable
s 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 AnimatedVectorDrawable
s 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 🥺
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="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/checked"
android:drawable="@drawable/ic_tab_home_24"
android:state_checked="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_tab_home_outline_24"
android:state_checked="false" />
<transition
android:drawable="@drawable/anim_tab_home_select"
android:fromId="@id/unchecked"
android:toId="@id/checked" />
<transition
android:drawable="@drawable/anim_tab_home_deselect"
android:fromId="@id/checked"
android:toId="@id/unchecked" />
</animated-selector>
So I made the ImageView
checkable, and passed in the selected tab state to each AndroidView
and here is the result:
Show me the code! 😅
@Composable
fun AnimatedIcon(
@DrawableRes animatedIcon: Int,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
AndroidView(
modifier = modifier.size(24.dp),
factory = { context ->
CheckableImageView(context).apply {
val drawable = ContextCompat.getDrawable(context, animatedIcon)
setImageDrawable(drawable)
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
refreshDrawableState()
}
}
companion object {
private val CHECKED_STATE_SET = intArrayOf(
android.R.attr.state_checked
)
}
}
@Composable
private fun BottomNavigationTabs(
navController: NavController,
onSetTab: (BottomNavItem) -> Unit,
) {
val items = listOf(
BottomNavItem.Home,
BottomNavItem.Games,
BottomNavItem.Players,
BottomNavItem.Menu,
)
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)
NavigationBarItem(
selected = isSelected,
onClick = { onSetTab(item) },
alwaysShowLabel = false,
label = {
Text(
text = title,
style = MaterialTheme.typography.labelSmall,
)
},
icon = {
AnimatedIcon(
item.icon,
isSelected = isSelected,
)
}
)
}
}
}
Conclusion
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