Creating Animated Navigation Bar using Android Vector Drawables in Jetpack Compose
Remember, the good old days of using android view system using XMLs. If yes, then probably you must have come across something called as Android Vector Drawables or AVD in short. Don’t worry, even if you have not heard about it then also this article won’t disappoint you.
Okay, so here’s a short story of why I am writing this and how did I come across this. Take a look at the screen recording shown below.
This is from a personal project that I am working on currently. Earlier, I remember using AVD (yeah, let’s call it AVD from now) in previous projects and I wanted to do the same with my new project. Strangely enough, I couldn’t find much resources so had to do a lot of figuring out on my own.
So, this is what we’ll build in this article.
What We’ll Build
We’ll create this above shown bottom navigation bar where:
- Each tab has an animated icon achieved through animated vector drawables that transitions between two states (e.g., “selected” and “unselected”)
- The current selection persists across screen rotations or app restarts using Compose’s state management
Why Choose Android Vector Drawables
Sure, with a lot of animation libraries and frameworks, we now have a multitude of such third-party APIs to work with and achieve animations through a simpler API. Yes, I am referring them as simpler, because they are easy to work with for beginners but here are some reasons where android vector drawables (AVD) shines.
- Full Native Support: AVDs are natively supported, that means no need of adding any third-party libraries and seamless integration with Jetpack Compose and XML-based UI development
- Smaller File Sizes: Animated Vector Drawables (AVDs) often have significantly smaller file sizes compared to Lottie animations. This is because AVDs leverage XML and use programmatic transformations instead of storing frame-by-frame animation data.
- Performance Benefits: They are well optimized for android rendering pipelines and have minimal impact on app performance due to hardware acceleration. Also, this avoids the overhead of parsing JSON files.
- Scalability: Nope, we aren’t talking about that “scalability”. Its just the graphics scalability that is superior in AVDs. Since, AVDs are resolution-independent, they scale perfectly across devices with different screen densities.
Although, there are few use cases which could be better handled using Lottie and I’ll be telling that at the end. Let’s first jump into the code and see how this works.
Dependency
Before starting, we need to add one extra dependency from Compose APIs that will help us in using the AVDs. Strangely enough, it is not added by default while creating the project.
Add the following line in libs.versions.toml
file.
androidx-compose-animation-graphics = { group = "androidx.compose.animation", name = "animation-graphics" }
Then, this library could be imported to your build.gradle
file.
implementation(libs.androidx.compose.animation.graphics)
That’s all. We are now ready to dive into the code.
The Code
Below is the composable function that I created for the custom animated navigation bar
@Composable
fun AnimatedNavigationBar() {
// Tracks the currently selected tab index.
var selectedTabIndex by rememberSaveable {
// TODO: Fetch this from saved preferences for the user's last visited destination.
mutableIntStateOf(0)
}
val navDestinations = listOf(
NavDestinations.HomeScreen,
NavDestinations.ListScreen,
NavDestinations.SettingScreen
)
SpeseTheme {
NavigationBar {
navDestinations.forEachIndexed { index, destination ->
// Load the animated vector resource for the icon.
val image = AnimatedImageVector.animatedVectorResource(destination.icon)
// Track whether the animation is at its "end" state.
var atEnd by rememberSaveable { mutableStateOf(selectedTabIndex == index) }
// Update the animation state whenever the selected tab index changes.
LaunchedEffect(selectedTabIndex) {
atEnd = selectedTabIndex == index
}
NavigationBarItem(
selected = selectedTabIndex == index,
onClick = {
selectedTabIndex = index
atEnd = !atEnd
},
icon = {
Icon(
painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd),
contentDescription = destination.label
)
},
)
}
}
}
}
Now, in case, if you are wondering what is NavDestinations then here’s the reference to the sealed
class I created for the destinations.
sealed class NavDestinations (val route: String, val icon: Int, val label: String) {
data object HomeScreen: SpeseNavDestinations("home", R.drawable.explore_anim, "Home")
data object ListScreen: SpeseNavDestinations("list", R.drawable.list_anim, "List")
data object SettingScreen: SpeseNavDestinations("settings", R.drawable.tuner_anim, "Settings")
}
How It Works
Let’s break this down step by step.
1. Navigation State Management
The selectedTabIndex
variable keeps track of the currently selected tab. We use rememberSaveable
so that the state persists across configuration changes (like screen rotations):
var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) }
Later, this will be updated in the onClick
event when a tab is clicked.
2. Animated Icons
Each tab’s icon is loaded as an AnimatedImageVector
:
val image = AnimatedImageVector.animatedVectorResource(destination.icon)
An animated vector drawable (AVD) allows you to define animations directly in your vector drawables (typically in XML files). In Compose, these can be animated using rememberAnimatedVectorPainter
:
Icon(
painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd),
contentDescription = destination.label
)
The atEnd
boolean controls whether the icon is in its start state (false
) or end state (true
). When atEnd
changes, the animation transitions smoothly between these two states.
3. Dynamic Animation State
The animation’s behavior is controlled using the atEnd
variable for each tab. Here's how it works:
Initialization
atEnd
is initialized with rememberSaveable
, and its value depends on whether the tab is the currently selected one:
var atEnd by rememberSaveable { mutableStateOf(selectedTabIndex == index) }
Updating State with LaunchedEffect
The LaunchedEffect
block ensures that atEnd
updates whenever the selected tab changes:
LaunchedEffect(selectedTabIndex) {
atEnd = selectedTabIndex == index
}
This guarantees that only the currently selected tab’s icon transitions to its “end state,” while others revert to their “start state.”
Handling Tab Clicks
When a tab is clicked, the following happens:
selectedTabIndex
is updated to reflect the newly selected tab.atEnd
is toggled to trigger the animation for the clicked tab:
onClick = {
selectedTabIndex = index
atEnd = !atEnd
}
This causes the UI to recompose, and the new animation state is reflected visually.
How Variables Change During Interaction
Let’s visualize the flow of state changes when a user clicks a tab:
Initial State
- selectedTaxIndex = 0 (Tab 0 is selected)
- atEnd for each tab:
- Tab 0 : true
- Tab 1 : false
- Tab 2 : false
Click Tab 1
- onClick updates selectedTabIndex = 1
- Recomposition triggers LaunchedEffect, which updates atEnd for each tab
- Tab 0 : false
- Tab 1 : true
- Tab 2 : false
Result
- Tab 1's icon animates to its "end state"
- Tab 0 and Tab 2 revert to their "start state"
This dynamic behaviour creates a visually engaging and interactive navigation bar.
In case, if you are wondering, where I got those animated icons from. Nope, I didn’t get them anywhere. I had to create all of them individually using an open-source tool called ShapeShifter. I’ll link some good YouTube tutorials on how to create such icons and will write more on how I did it from a beginner's point of view.
Conclusion
Android Vector Drawables are great. They are smooth. Perform really well. And moreover, while trying to find animations for my nav bar, I did visit LottieFiles and tried to find good animations. But I didn’t find that is minimalistic and well suited for the app. In designing interfaces, the animations should be subtle in areas like a nav bar, which I didn’t find in LottieFiles. On a side note, yes, I could create such animations on lottie files as well ( I have created a few). But, in order to do that, we again need to learn Adobe After Effects and install a bunch of plugins and hence the learning curve becomes really high. Surely, Lottie has its use cases, like logo animation and very complex animation. For such use cases, surely use Lottie. But each framework has its own place to shine.
If you’ve come this far. Thanks for reading the article.
P.S: Below is a sneak peak of what ShapeShifter looks like and also the YouTube tutorial on how to use this tool to create the animated vector drawable you like.
Well, you can also search around in Google Compose Samples and search their codebases for a few as well. That’s what I did to learn initially.