As far as I know there is no simple way to make animated transitions when use Navigation Component BottomNavigationBar. The way described in official guide does not support animated transitions. So I came across some similar questions here on StackOverflow that still don't help.
I did it according to the question mentioned earlier, but it still doesn't work and error occurs:
java.lang.IllegalArgumentException: Navigation action/destination com.example.banktask3:id/action_newsFragment_to_friendsFragment cannot be found from the current destination Destination(com.example.banktask3:id/friendsFragment) label=fragment_friends class=com.example.banktask3.presentation.friends.FriendsFragment
I suppose that the problem is that my app doesn't know what fragment is opened at the moment. I tried to use selected bottom menu item using binding.bottomNavView.selectedItemId
, but it still doesn't work. How can I fix it? I suppose that I need to get current fragment from MainActivity and then check in when
what fragment is shown at the moment and decide, what action is to be taken.
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setNavigation()
}
private fun setNavigation(){
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container_view) as NavHostFragment
navController = navHostFragment.navController
binding.bottomNavView.setOnNavigationItemSelectedListener { item ->
selectFragment(item)
false
}
}
private fun selectFragment(item: MenuItem) {
if (binding.bottomNavView.selectedItemId == -1)
navController.navigate(item.itemId)
else
navController.navigate(
when (item.itemId) {
R.id.newsFragment ->
if (binding.bottomNavView.selectedItemId == R.id.friendsFragment)
R.id.action_friendsFragment_to_newsFragment
else
R.id.action_groupsFragment_to_newsFragment
R.id.friendsFragment ->
if (binding.bottomNavView.selectedItemId == R.id.groupsFragment)
R.id.action_groupsFragment_to_friendsFragment
else
R.id.action_newsFragment_to_friendsFragment
R.id.groupsFragment ->
if (binding.bottomNavView.selectedItemId == R.id.newsFragment)
R.id.action_newsFragment_to_groupsFragment
else
R.id.action_friendsFragment_to_groupsFragment
else -> item.itemId
})
binding.bottomNavView.selectedItemId = item.itemId
// uncheck the other items.
for (i in 0 until binding.bottomNavView!!.menu.size()) {
val menuItem = binding.bottomNavView!!.menu.getItem(i)
if (menuItem.itemId == item.itemId) menuItem.isChecked = true
}
}
CodePudding user response:
Since the default animation was still playing, I opened the code for NavigationUI.java. Which is the following:
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
NavOptions.Builder builder = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim);
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(),
false);
}
NavOptions options = builder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
As you can tell, in the NavOptions.Builder, the default anims are being set.
The workaround you used was not satisfactory for me. So, I took the liberty in creating a BottomNavigationUI class that would do the function of the NavigationUI, but using custom anims when available.
BottomNavigationUI.class
// don't forget your package
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.NavAction;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import androidx.navigation.NavGraph;
import androidx.navigation.NavOptions;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.lang.ref.WeakReference;
import java.util.Set;
public class BottomNavigationUI {
private BottomNavigationUI() {
}
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
int resId = item.getItemId();
Bundle args = null;
NavOptions options;
NavOptions.Builder optionsBuilder = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim);
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
optionsBuilder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
final NavAction navAction = navController.getCurrentDestination().getAction(resId);
if (navAction != null) {
NavOptions navOptions = navAction.getNavOptions();
// Note : You can Add *setLaunchSingleTop* and *setPopUpTo* from *navOptions* to *builder*
if (navOptions.getEnterAnim() != -1) {
optionsBuilder.setEnterAnim(navOptions.getEnterAnim());
}
if (navOptions.getExitAnim() != -1) {
optionsBuilder.setExitAnim(navOptions.getExitAnim());
}
if (navOptions.getPopEnterAnim() != -1) {
optionsBuilder.setPopEnterAnim(navOptions.getPopEnterAnim());
}
if (navOptions.getPopExitAnim() != -1) {
optionsBuilder.setPopExitAnim(navOptions.getPopExitAnim());
}
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
args = new Bundle();
args.putAll(navActionArgs);
}
}
options = optionsBuilder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(resId, args, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public static void setupWithNavController(
@NonNull final BottomNavigationView bottomNavigationView,
@NonNull final NavController navController) {
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
return onNavDestinationSelected(item, navController);
}
});
final WeakReference<BottomNavigationView> weakReference =
new WeakReference<>(bottomNavigationView);
navController.addOnDestinationChangedListener(
new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
BottomNavigationView view = weakReference.get();
if (view == null) {
navController.removeOnDestinationChangedListener(this);
return;
}
Menu menu = view.getMenu();
for (int h = 0, size = menu.size(); h < size; h ) {
MenuItem item = menu.getItem(h);
if (matchDestination(destination, item.getItemId())) {
item.setChecked(true);
}
}
}
});
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean matchDestination(@NonNull NavDestination destination,
@IdRes int destId) {
NavDestination currentDestination = destination;
while (currentDestination.getId() != destId && currentDestination.getParent() != null) {
currentDestination = currentDestination.getParent();
}
return currentDestination.getId() == destId;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean matchDestinations(@NonNull NavDestination destination,
@NonNull Set<Integer> destinationIds) {
NavDestination currentDestination = destination;
do {
if (destinationIds.contains(currentDestination.getId())) {
return true;
}
currentDestination = currentDestination.getParent();
} while (currentDestination != null);
return false;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static NavDestination findStartDestination(@NonNull NavGraph graph) {
NavDestination startDestination = graph;
while (startDestination instanceof NavGraph) {
NavGraph parent = (NavGraph) startDestination;
startDestination = parent.findNode(parent.getStartDestination());
}
return startDestination;
}
}
Now, with that, you need to make some changes.
MainActivity : After
BottomNavigationUI.setupWithNavController(bottomNavigationView, navController)
bottomNavigationView.setOnNavigationItemReselectedListener { false }
nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/nav_graph"
app:startDestination="@id/home_fragment">
<fragment
android:id="@ id/home_fragment"
android:name="com.example.android.navbottomsample.HomeFragment"
android:label="HomeFragment"
tools:layout="@layout/fragment_home">
<action
app:launchSingleTop="true"
android:id="@ id/schedule_fragment"
app:destination="@id/schedule_fragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@ id/home_fragment" />
</fragment>
<fragment
android:id="@ id/schedule_fragment"
android:name="com.example.android.navbottomsample.ScheduleFragment"
android:label="ScheduleFragment"
tools:layout="@layout/fragment_schedule">
<action
android:id="@ id/home_fragment"
app:destination="@id/home_fragment"
app:enterAnim="@anim/slide_in_left"
app:exitAnim="@anim/slide_out_right"
app:popEnterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_left"
app:popUpTo="@ id/home_fragment" />
</fragment>
</navigation>