Home > Back-end >  How can I create animated transitions with Bottom Navigation?
How can I create animated transitions with Bottom Navigation?

Time:04-29

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>
  • Related