This Tutorial will discuss how to implement custom transition animation when using Navigation Component provided by Android Jetpack and TabLayout.
This tutorial assumes that you are familiar with the following:
- Android Jetpack Navigation Component
- TabLayout
If you are familiar with Android Jetpack Navigation Component, you will find that there is no direct implementation with the TabLayout. Actually, The Navigation Component intentionally doesn't support direct implementation with TabLayout as "Navigation focuses on elements that affect the back stack and tabs do not affect the back stack - you should continue to manage tabs with a ViewPager and TabLayout" Google Issue Tracker.
Here is How you could manage Tabs with ViewPager
- Navigation Component with TabLayout
- Applying Custom Transition Animation
For the first case scenario, you may have 5 tabs in the TabLayout to navigate between available destinations. We will supposed that you have created the TabLayout in the XML file as below
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Home" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Request" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chat" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Notification" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Profile" />
</com.google.android.material.tabs.TabLayout>
So, Here is the Steps to achieve the desired behavior in the first case scenario;
-
Create a NavOption object. You have to make sure of creating only one instance of the same destination target (the destination fragment you are navigating to) by adding this parameter to the NavOption Object
.setLaunchSingleTop(true)
-
You will also need to make sure that the navigation BackStack is cleared with every transition so that when you press the back button, you go for the start destination fragment, not the previous selected fragment. This can be achieved by adding the following parameters to the NavOption Object
.setPopUpTo(navController.getGraph().getStartDestination(), false)
-
Pass the NavOption Object to the NavController.navigate() method to be applied for the navigation operation navController.navigate(item.getItemId(), null, navOptions);
-
Override the addOnTabSelectedListener() method to control what happen when you select a tab
-
Create s Switch/Case Statement to differentiate between different tabs based on their positions
-
Navigate to the desired destination fragment based on the tab position and don't forget to pass your NavOptions object.
-
Override the onBackPressed() method to check the tab related to the start destination fragment.
Here is a complete code snippet
@Override
protected void onCreate(Bundle savedInstanceState) {
....
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavOptions navOptions = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(navController.getGraph().getStartDestination(), false)
.build();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
switch (tab.getPosition()) {
case 0:
navController.navigate(R.id.homeFragment, null, navOptions);
break;
case 1:
navController.navigate(R.id.requestsFragment, null, navOptions);
break;
case 2:
navController.navigate(R.id.chatFragment, null, navOptions);
break;
case 3:
navController.navigate(R.id.notificationsFragment, null, navOptions);
break;
case 4:
navController.navigate(R.id.profileFragment, null, navOptions);
break;
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
@Override
public void onBackPressed() {
tabLayout.getTabAt(0).select();
super.onBackPressed();
}
For this case scenario, you have the exact first scenario but you also want to apply a custom transition animation.
Here is the Steps to achieve the desired behavior in the Second case scenario;
-
Create a NavOption object and pass the custom transition animation to it using the following methods
.setEnterAnim(R.anim.slide_in_right) .setExitAnim(R.anim.slide_out_left) .setPopEnterAnim(R.anim.slide_in_right) .setPopExitAnim(R.anim.slide_out_left)
-
Create a NavOption object. You have to make sure of creating only one instance of the same destination target (the destination fragment you are navigating to) by adding this parameter to the NavOption Object
.setLaunchSingleTop(true)
-
You will also need to make sure that the navigation BackStack is cleared with every transition so that when you press the back button, you go for the start destination fragment, not the previous selected fragment. This can be achieved by adding the following parameters to the NavOption Object
.setPopUpTo(navController.getGraph().getStartDestination(), false)
-
Pass the NavOption Object to the NavController.navigate() method to be applied for the navigation operation navController.navigate(item.getItemId(), null, navOptions);
-
Override the addOnTabSelectedListener() method to control what happen when you select a tab
-
Create s Switch/Case Statement to differentiate between different tabs based on their positions
-
Navigate to the desired destination fragment based on the tab position and don't forget to pass your NavOptions object.
-
Override the onBackPressed() method to check the tab related to the start destination fragment.
Here is a complete code snippet
@Override
protected void onCreate(Bundle savedInstanceState) {
....
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavOptions navOptions = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.setPopEnterAnim(R.anim.slide_in_right)
.setPopExitAnim(R.anim.slide_out_left)
.setPopUpTo(navController.getGraph().getStartDestination(), false)
.build();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
switch (tab.getPosition()) {
case 0:
navController.navigate(R.id.homeFragment, null, navOptions);
break;
case 1:
navController.navigate(R.id.requestsFragment, null, navOptions);
break;
case 2:
navController.navigate(R.id.chatFragment, null, navOptions);
break;
case 3:
navController.navigate(R.id.notificationsFragment, null, navOptions);
break;
case 4:
navController.navigate(R.id.profileFragment, null, navOptions);
break;
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
@Override
public void onBackPressed() {
tabLayout.getTabAt(0).select();
super.onBackPressed();
}
If you apply this tutorial, You may achieve your desired behavior but you will violate two intended behavior
- Cross fade animation between BottomNavigationView items is an intended behavior to follow the Material GuideLines.
- Navigation focuses on elements that affect the back stack and tabs do not affect the back stack.
Navigation-Component-BottomNavigationView-with-Custom-Transition-Animation-and-Navigation-Listener
Navigation-Component-DrawerLayout-with-Custom-Transition-Animation-and-Custom-Navigation-Listener
Navigation-Component-OptionsMenu-with-Custom-Transition-Animation-and-Custom-Navigation-Listener
https://developer.android.com/guide/navigation/navigation-getting-started
https://developer.android.com/guide/navigation/navigation-ui
https://codelabs.developers.google.com/codelabs/android-navigation/index.html#0
https://developer.android.com/guide/navigation/navigation-swipe-view