Activity-Based Bottom Navigation Using Shared Element Transition with BottomNavigationView

Using
BottomNavigationView
with fragments is well documented. The Jetpack Navigation Component supports fragment-based navigation via theBottomNavigationView
for top level destinations in the app. However, many apps, especially legacy Android code bases, still primarily use activities as their top level destinations. Here, we explore a potential solution using Shared Element Transition on theBottomNavigationView
.
There are many reasons why migrating to fragment-based navigation can be a complex task. Sometimes, it isn’t feasible or practical to refactor an Android app that uses activity-based navigation. For instance, many legacy apps have activities that use different launch modes, deep linking logic, etc.
With apps that use activity-based navigation, migrating to the relatively newer BottomNavigationView
can result in UI jank. For instance, each activity reachable by the BottomNavigationView
launches a new activity, which in turn has to create a new BottomNavigationView
each time it is launched. If we stick with the default activity transitions, this can result in the BottomNavigationView
seeming to jump into view with every activity transition. Ideally, we’d like the BottomNavigationView
to be “sticky”, or at least give the illusion of it persisting across activities.
In this article, I introduce a potential solution to migrate to BottomNavigationView
navigation for apps that are heavily activity based.
Shared element transition is not new. It was introduced in Android Lollipop. It’s used for animating transitions of views when switching between activities. For instance, take this classic example from the Android developer documentation where an ImageView
and a TextView
are the shared elements between the two screens.

Exploring our options
1. A hack?
There is a hacky solution, but it uses overridePendingTransitions(0, 0)
, which essentially abolishes all activity transition animations. This means that while the BottomNavigationBar
visually appears to be sticky, navigating between each activity looks abrupt, each activity snapping into view without any easing. While this is an easy solve, it may not produce the desired result, as seen in the video below.

2. Proposed solution using Shared Element Transition
What if we used activity transitions with shared elements on the BottomNavigationBar
? If we think about it, it makes some sense. The common or shared element between the activities is the BottomNavigationBar
itself. Although we’re not necessarily interested in animating the BottomNavigationBar
in any way, it is the common or shared element between the activities. It turns out that this solution is potentially viable for projects that cannot migrate, or need to defer migration to fragment-based bottom navigation. Let’s get started.
Implementation
First, add the following attributes to your app theme. windowEnterTransition
and windowExitTransition
are only required if you want to override the default window transitions provided by the Android system. If you’re not interested in changing those, you can leave these out. The windowSharedElementEnterTransition
and windowSharedElementExitTransition
values are not really important here, since we’re not actually interested in animating any aspect of the BottomNavigationBar
. However, we still need to specify this so that that the app understands that there is a shared element. You can use any of the provided standard transitions provided by the Android platform: change_bounds, change_image_transform, change_transform, change_image_transform, change_clip_bounds
. You could also create a no-op transition class and use that if you wish.
<<style name="AppTheme" parent="android:Theme.Material">
<!-- enable window activity transitions (REQUIRED) -->
<item name="android:windowActivityTransitions">true</item>
<!-- specify enter and exit transitions (OPTIONAL) -->
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>
<!-- specify shared element transitions -->
<item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform</item>
<item name="android:windowSharedElementExitTransition">
@transition/change_image_transform</item>
</style>
Depending on which standard transition you used, you’ll need to define it in your res/transition
folder, for instance:
<!-- res/transition/change_image_transform.xml -->
<transitionSet xmlns:tools="http://schemas.android.com/tools"
tools:ignore="ResourceName">
<changeImageTransform/>
</transitionSet>
Next, define a shared element name for the widget, in our case the BottomNavigationView in your donottranslate.xml
file.
<!-- res/values/donottranslate.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="nav_view_shared_element_transition_name">navigation_view_shared_element</string>
</resources>
Now, set that string as the android:transitionName
attribute in your BottomNavigationView
. You will see later why this is necessary
<!-- res/layout/activity_main.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- Rest of your activity content -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
android:transitionName="@string/nav_view_shared_element_transition_name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
</ConstraintLayout>
Now, we are ready to use the above to navigate using our BottomNavigationView
.
Whenever you start a new activity from tapping on the BottomNavigationBar, you will need to set up the scene transition animation for the BottomNavigationBar
. Note that I am using the base class NavigationBarView
here in case your app makes use of the NavigationRailView
on landscape or larger screens.
fun NavigationBarView.startActivityWithNavBarSharedTransition(
activity:Activity,
intent: Intent
) {
val options = ActivityOptions.makeSceneTransitionAnimation(
activity,
this,
activity.getString(R.string.nav_view_shared_element_transition_name)
).toBundle()
activity.startActivity(intent, options)
}
This is what your MainActivity
could look like:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<NavigationBarView>(R.id.nav_view).apply {
selectedItemId = R.id.navigation_home
setOnItemSelectedListener {
when (it.itemId) {
R.id.navigation_notifications -> {
startActivityWithNavBarSharedTransition(
this@MainActivity,
Intent(applicationContext, NotificationsActivity::class.java)
)
true
}
R.id.navigation_dashboard -> {
startActivityWithNavBarSharedTransition(
this@MainActivity,
Intent(applicationContext, DashboardActivity::class.java)
)
true
}
R.id.navigation_home -> true
else -> false
}
}
}
}
}
And voila, the app gives the illusion that the BottomNavigationBar
is sticky, and we’ve kept the system’s activity transitions intact:

GitHub
You can find the source code for the demo here: https://github.com/kelvinwatson/activity-based-bottom-navigation