How do I define default animations for Navigation Actions?
Here's my solution, and it worked well in my app.
public void navigate(int resId, Bundle bundle) {
NavController navController = getNavController();
if (navController == null) return;
NavDestination currentNode;
NavBackStackEntry currentEntry = navController.getCurrentBackStackEntry();
if (currentEntry == null) currentNode = navController.getGraph();
else currentNode = currentEntry.getDestination();
final NavAction navAction = currentNode.getAction(resId);
final NavOptions navOptions;
if (navAction == null || navAction.getNavOptions() == null) navOptions = ExampleUtil.defaultNavOptions;
else if (navAction.getNavOptions().getEnterAnim() == -1
&& navAction.getNavOptions().getPopEnterAnim() == -1
&& navAction.getNavOptions().getExitAnim() == -1
&& navAction.getNavOptions().getPopExitAnim() == -1) {
navOptions = new NavOptions.Builder()
.setLaunchSingleTop(navAction.getNavOptions().shouldLaunchSingleTop())
.setPopUpTo(resId, navAction.getNavOptions().isPopUpToInclusive())
.setEnterAnim(ExampleUtil.defaultNavOptions.getEnterAnim())
.setExitAnim(ExampleUtil.defaultNavOptions.getExitAnim())
.setPopEnterAnim(ExampleUtil.defaultNavOptions.getPopEnterAnim())
.setPopExitAnim(ExampleUtil.defaultNavOptions.getPopExitAnim())
.build();
} else navOptions = navAction.getNavOptions();
navController.navigate(resId, bundle, navOptions);
}
R.anim has the default animations defined (as final):
nav_default_enter_anim
nav_default_exit_anim
nav_default_pop_enter_anim
nav_default_pop_exit_anim
in order to change this behavior, you would have to use custom NavOptions,
because this is where those animation are being assigned to a NavAction.
one can assign these with the NavOptions.Builder:
protected NavOptions getNavOptions() {
NavOptions navOptions = new NavOptions.Builder()
.setEnterAnim(R.anim.default_enter_anim)
.setExitAnim(R.anim.default_exit_anim)
.setPopEnterAnim(R.anim.default_pop_enter_anim)
.setPopExitAnim(R.anim.default_pop_exit_anim)
.build();
return navOptions;
}
most likely one would need to create a DefaultNavFragment
, which extends class androidx.navigation.fragment (the documentation there does not seem completed yet).
So you can pass these NavOptions
to the NavHostFragment
like this:
NavHostFragment.findNavController(this).navigate(R.id.your_action_id, null, getNavOptions());
alternatively... when looking at the attrs.xml
of that package; these animations are style-able:
<resources>
<declare-styleable name="NavAction">
<attr name="enterAnim" format="reference"/>
<attr name="exitAnim" format="reference"/>
<attr name="popEnterAnim" format="reference"/>
<attr name="popExitAnim" format="reference"/>
...
</declare-styleable>
</resources>
this means, one can define the according styles - and define these, as part of the theme...
one can define them in styles.xml
:
<style name="Theme.Default" parent="Theme.AppCompat.Light.NoActionBar">
<!-- these should be the correct ones -->
<item name="NavAction_enterAnim">@anim/default_enter_anim</item>
<item name="NavAction_exitAnim">@anim/default_exit_anim</item>
<item name="NavAction_popEnterAnim">@anim/default_pop_enter_anim</item>
<item name="NavAction_popExitAnim">@anim/default_pop_exit_anim</item>
</style>
One can also define the default animations in res/anim
:
res/anim/nav_default_enter_anim.xml
res/anim/nav_default_exit_anim.xml
res/anim/nav_default_pop_enter_anim.xml
res/anim/nav_default_pop_exit_anim.xml
I found solution that requires extending NavHostFragment
. It's similar to Link182 but less involved in code. Most often it will require to change all xml defaultNavHost fragments names from standard:
<fragment
app:defaultNavHost="true"
...
android:name="androidx.navigation.fragment.NavHostFragment"
to:
<fragment
app:defaultNavHost="true"
...
android:name="your.app.package.fragments.NavHostFragmentWithDefaultAnimations"
Code for NavHostFragmentWithDefaultAnimations
:
package your.app.package.fragments
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import androidx.navigation.*
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.NavHostFragment
import your.app.package.R
// Those are navigation-ui (androidx.navigation.ui) defaults
// used in NavigationUI for NavigationView and BottomNavigationView.
// Set yours here
private val defaultNavOptions = navOptions {
anim {
enter = R.animator.nav_default_enter_anim
exit = R.animator.nav_default_exit_anim
popEnter = R.animator.nav_default_pop_enter_anim
popExit = R.animator.nav_default_pop_exit_anim
}
}
private val emptyNavOptions = navOptions {}
class NavHostFragmentWithDefaultAnimations : NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
// this replaces FragmentNavigator
FragmentNavigatorWithDefaultAnimations(requireContext(), childFragmentManager, id)
)
}
}
/**
* Needs to replace FragmentNavigator and replacing is done with name in annotation.
* Navigation method will use defaults for fragments transitions animations.
*/
@Navigator.Name("fragment")
class FragmentNavigatorWithDefaultAnimations(
context: Context,
manager: FragmentManager,
containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
// this will try to fill in empty animations with defaults when no shared element transitions are set
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
val shouldUseTransitionsInstead = navigatorExtras != null
val navOptions = if (shouldUseTransitionsInstead) navOptions
else navOptions.fillEmptyAnimationsWithDefaults()
return super.navigate(destination, args, navOptions, navigatorExtras)
}
private fun NavOptions?.fillEmptyAnimationsWithDefaults(): NavOptions =
this?.copyNavOptionsWithDefaultAnimations() ?: defaultNavOptions
private fun NavOptions.copyNavOptionsWithDefaultAnimations(): NavOptions =
let { originalNavOptions ->
navOptions {
launchSingleTop = originalNavOptions.shouldLaunchSingleTop()
popUpTo(originalNavOptions.popUpTo) {
inclusive = originalNavOptions.isPopUpToInclusive
}
anim {
enter =
if (originalNavOptions.enterAnim == emptyNavOptions.enterAnim) defaultNavOptions.enterAnim
else originalNavOptions.enterAnim
exit =
if (originalNavOptions.exitAnim == emptyNavOptions.exitAnim) defaultNavOptions.exitAnim
else originalNavOptions.exitAnim
popEnter =
if (originalNavOptions.popEnterAnim == emptyNavOptions.popEnterAnim) defaultNavOptions.popEnterAnim
else originalNavOptions.popEnterAnim
popExit =
if (originalNavOptions.popExitAnim == emptyNavOptions.popExitAnim) defaultNavOptions.popExitAnim
else originalNavOptions.popExitAnim
}
}
}
}
You can change animations in nav graph xml or in code through passing navOptions. To disable default animations pass navOptions with anim values of 0 or pass navigatorExtras (setting shared transitions).
Tested for version:
implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"
For version 2.5.2
fun navigate(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?)
has to be override as well.