57

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.

DAA
  • 1,346
  • 2
  • 11
  • 19
Smedegaard
  • 727
  • 1
  • 6
  • 18
  • I have not seen complete document of navigation architecture component, but I think there must be some style like feature as we use generally for other UI components, for making default animation in actions. – Aseem Sharma May 23 '18 at 08:24
  • You can vote for [corresponding](https://issuetracker.google.com/issues/111759776) [issues](https://issuetracker.google.com/issues/178291654) – gmk57 Feb 12 '21 at 14:29

6 Answers6

51

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
Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • 2
    can you post a sample style to be applied as theme? I tried your second approach but it doesn't work. – Hafez Divandari Sep 23 '18 at 20:31
  • @HafezDivandari see https://developer.android.com/guide/topics/ui/look-and-feel/themes - you need to define those styles and pass a resource (the animation); I've only shown that part of the `attrs.xml` in order to tell the name of the style-able and it's styles (the theme also needs to be assigned to the application or activity). – Martin Zeitler Sep 23 '18 at 20:33
  • @HafezDivandari also added an example; just unsure about the namespace of `parent`; think it is not `@android:style/`, because it's library styles and not framework styles. – Martin Zeitler Sep 23 '18 at 20:45
  • error: Cannot resolve symbol 'NavAction', I applied this theme (without paret="NavAction") to navigation tag but it doesn't work. – Hafez Divandari Sep 23 '18 at 20:50
  • @HafezDivandari here they are also declared: https://developer.android.com/reference/androidx/navigation/ui/R.styleable#navaction_popenteranim - the name might be `NavAction_popEnterAnim`, without parent. – Martin Zeitler Sep 23 '18 at 20:55
  • 10
    `NavAction_enterAnim` causes error on build, error: style attribute 'attr/NavAction_enterAnim (aka com.myapp.myapp:attr/NavAction_enterAnim)' not found. – Hafez Divandari Sep 27 '18 at 21:34
  • 4
    This won't work with `parent="Theme.MaterialComponents..."` I'm guessing they need to add those to the MaterialComponents styles still. – Peter Keefe Feb 07 '19 at 16:21
  • 3
    doesn't seem to be working for me with material theme or the theme listed in this example – William Reed Jun 05 '19 at 13:32
  • 1
    You have 2 dead links, and also it would be nice to see what do you mean with your first approach with some code, it is like a util function `getNavOptions ` that always returns that `NavOptions`? If it so, then what is the benefit of doing that every time you navigate instead of just having at your nav graph? Is there a way to overwrite the NavOptions using just the classes that you just mentioned on your first method? Do you need to combine it with the styles? Or the second approach is to extend the NavHostFragment and overwrite what in order to have that `NavOption` default? – desgraci Jun 11 '19 at 08:35
  • for those who want these to use in xml `?attr/enterAnim` `?attr/exitAnim` `?attr/popEnterAnim` `?attr/popExitAnim` – Vahid Sep 30 '20 at 13:31
  • Setting theme attributes didn't work for me and it looks like it can't work looking at the sources for version 2.3.1 of navigation lib https://androidx.tech/artifacts/navigation/navigation-runtime/2.3.1-source/androidx/navigation/NavInflater.java.html – fada21 Jan 16 '21 at 22:47
  • @fada21 The them comes from the `Activity` context; and the inflater doesn't care how it's styled. – Martin Zeitler Jan 16 '21 at 22:51
  • @MartinZeitler inflateAction can't know about the theme as it is getting attrs by `final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction);` which doesn't care about style or theme. `Resources.Theme#obtainStyledAttributes` does. – fada21 Jan 16 '21 at 23:10
  • 3
    BEWARE, this wil override the NavOptions stated in XML, which means you will lose properties like singleTop, popUpTo, and default arguments. – Risal Fajar Amiyardi Jun 01 '21 at 15:48
  • Styling something should only override the styled values; make sure to have a parent theme... if not, it may appear alike "as if", but those properties might not be there, to begin with. – Martin Zeitler Jun 02 '21 at 12:59
  • 1
    This method should not be used as a util as it will replace XML attributes related to NavOptions such as singleTop, popUpTo, popUpToInclusive as told by @RisalFajarAmiyardi . I wasted 2 hours on this issue – Raghav Satyadev Mar 21 '22 at 21:01
12

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.

fada21
  • 3,188
  • 1
  • 22
  • 21
  • 3
    This is great! Would be cool if you created a small library with this and publish it on jitpack.io – binarynoise Aug 01 '21 at 14:39
  • 2
    I mean, things like this hurt me on a visceral level. Don't get me wrong, this is a practical solution but it just demonstrates the incompetence of Navigation Ui original authors that designed the original library in such a way so for something trivial as global animation we have to go this level of customization. – YaMiN Mar 27 '22 at 07:23
2

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);
    }
snakecharmerb
  • 47,570
  • 11
  • 100
  • 153
John 6
  • 99
  • 1
  • 5
1

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())
Suryavel TR
  • 3,576
  • 1
  • 22
  • 25
  • I just would like know, how to call it! Explain NavDirections for me. – AllanRibas Feb 08 '22 at 05:17
  • @AllanRibas "How to call it?" It is in the answer. findNavController().navigateWithDefaultAnimation(HomeFragmentDirections.homeToProfile()) instead of just navigate. HomeFragmentDirectons is the direction class generated by safeargs and it contains destination (e.g. homeToProfile) – Suryavel TR Feb 09 '22 at 06:10
-1

As said, R.anim has the default animations defined:

  • nav_default_enter_anim

  • nav_default_exit_anim

  • nav_default_pop_enter_anim

  • nav_default_pop_exit_anim

But you can easily override them.

Just create your own four anim resources with the same names in your app module (just to clarify, the id of one of them is your.package.name.R.anim.nav_default_enter_anim) and write what animation you'd like.

nbaroz
  • 1,795
  • 13
  • 14
  • 5
    Only `NavigationUI` class uses these resources, other classes like `FragmentNavigator` don't. So this solution doesn't work on navigate with actions as requested on the question. – Hafez Divandari Sep 27 '18 at 21:29
-1

It is possible with custom androidx.navigation.fragment.Navigator.

I will demonstrate how to override fragment navigation. Here is our custom navigator. Pay attention to setAnimations() method

@Navigator.Name("fragment")
class MyAwesomeFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager, // Should pass childFragmentManager.
    private val containerId: Int
): FragmentNavigator(context, manager, containerId) {
private val backStack by lazy {
    this::class.java.superclass!!.getDeclaredField("mBackStack").let {
        it.isAccessible = true
        it.get(this) as ArrayDeque<Integer>
    }
}

override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?): NavDestination? {
    if (manager.isStateSaved) {
        logi("Ignoring navigate() call: FragmentManager has already"
                + " saved its state")
        return null
    }
    var className = destination.className
    if (className[0] == '.') {
        className = context.packageName + className
    }
    val frag = instantiateFragment(context, manager,
            className, args)
    frag.arguments = args
    val ft = manager.beginTransaction()

    navOptions?.let { setAnimations(it, ft) }

    ft.replace(containerId, frag)
    ft.setPrimaryNavigationFragment(frag)

    @IdRes val destId = destination.id
    val initialNavigation = backStack.isEmpty()
    // TODO Build first class singleTop behavior for fragments
    val isSingleTopReplacement = (navOptions != null && !initialNavigation
            && navOptions.shouldLaunchSingleTop()
            && backStack.peekLast()?.toInt() == destId)

    val isAdded: Boolean
    isAdded = if (initialNavigation) {
        true
    } else if (isSingleTopReplacement) { // Single Top means we only want one 
instance on the back stack
        if (backStack.size > 1) { // If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
            manager.popBackStack(
                    generateBackStackName(backStack.size, backStack.peekLast()!!.toInt()),
                    FragmentManager.POP_BACK_STACK_INCLUSIVE)
            ft.addToBackStack(generateBackStackName(backStack.size, destId))
        }
        false
    } else {
        ft.addToBackStack(generateBackStackName(backStack.size + 1, destId))
        true
    }
    if (navigatorExtras is Extras) {
        for ((key, value) in navigatorExtras.sharedElements) {
            ft.addSharedElement(key!!, value!!)
        }
    }
    ft.setReorderingAllowed(true)
    ft.commit()
    // The commit succeeded, update our view of the world
    return if (isAdded) {
        backStack.add(Integer(destId))
        destination
    } else {
        null
    }
}

private fun setAnimations(navOptions: NavOptions, transaction: FragmentTransaction) {
    transaction.setCustomAnimations(
            navOptions.enterAnim.takeIf { it != -1 } ?: android.R.anim.fade_in,
            navOptions.exitAnim.takeIf { it != -1 } ?: android.R.anim.fade_out,
            navOptions.popEnterAnim.takeIf { it != -1 } ?: android.R.anim.fade_in,
            navOptions.popExitAnim.takeIf { it != -1 } ?: android.R.anim.fade_out
    )
}

private fun generateBackStackName(backStackIndex: Int, destId: Int): String? {
    return "$backStackIndex-$destId"
}
}

In the next step we have to add the navigator to NavController. Here is an example how to set it:

 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer)!!
    with (findNavController(R.id.fragmentContainer)) {
        navigatorProvider += MyAwesomeFragmentNavigator(this@BaseContainerActivity, navHostFragment.childFragmentManager, R.id.fragmentContainer)
        setGraph(navGraphId)
    }
}

And nothing special in xml :)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<fragment
    android:id="@+id/fragmentContainer"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true" />
</LinearLayout>

Now each fragment from graph will have alpha transitions

Link182
  • 733
  • 6
  • 15