I'm using Android Studio 3.2 Canary 14 and The Navigation Architecture Component. With this you can define transition animations pretty much as you would when using Intents.
But the animations are set as properties of the actions in the navigation graph, like so:
<fragment
android:id="@+id/startScreenFragment"
android:name="com.example.startScreen.StartScreenFragment"
android:label="fragment_start_screen"
tools:layout="@layout/fragment_start_screen" >
<action
android:id="@+id/action_startScreenFragment_to_findAddressFragment"
app:destination="@id/findAddressFragment"
app:enterAnim="@animator/slide_in_right"
app:exitAnim="@animator/slide_out_left"
app:popEnterAnim="@animator/slide_in_left"
app:popExitAnim="@animator/slide_out_right"/>
</fragment>
This gets tedious to define for all actions in the graph!
Is there a way to define a set of animations as default, on actions?
I've had no luck using styles for this.
They help users orient themselves by expressing your app's hierarchy, using movement... Navigation transitions use motion to guide users between two screens in your app. They help users orient themselves by expressing your app's hierarchy, using movement to indicate how elements are related to one another.
popUpTo and popUpToInclusive For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.
To animate the transition between fragments, or to animate the process of showing or hiding a fragment you use the Fragment Manager to create a Fragment Transaction . Within each Fragment Transaction you can specify in and out animations that will be used for show and hide respectively (or both when replace is used).
The enterAnim and exitAnim is applied when navigating to or from the destination the "regular way", while popEnterAnim is applied to the destination when it is shown as a result of the destination "above" it being popped from the backstack.
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.
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);
}
I have created the extension and called it instead of invoking navigation
wherever required.
fun NavController.navigateWithDefaultAnimation(directions: NavDirections) {
navigate(directions, navOptions {
anim {
enter = R.anim.anim_fragment_enter_transition
exit = R.anim.anim_fragment_exit_transition
popEnter = R.anim.anim_fragment_pop_enter_transition
popExit = R.anim.anim_fragment_pop_exit_transition
}
})
}
findNavController().navigateWithDefaultAnimation(HomeFragmentDirections.homeToProfile())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With