ga('send', 'pageview');
Categories
Blogg

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="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:

Material 3 NavigationBar with AnimatedStateListDrawables

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

Resources/links

https://fvilarino.medium.com/creating-an-animated-selector-in-jetpack-compose-669066dfc01b
Animated Selectable Item with Jetpack Compose | Android Studio Tutorial
https://stackoverflow.com/questions/68672046/how-to-use-animated-vector-drawable-in-compose

By Peter Törnhult

Android utvecklare, Techcoach, Scrum master

Leave a Reply

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