Reusable animations within Android navigation library nav graph

517 Views Asked by At

Wondering if there is a way to create a reusable set of the animations for the nav graph. Something like a style that contains all of the animation properties rather than repeating the same ones over and over.

What I have:

<action
        android:id="@+id/toFragmentA"
        app:destination="@id/fragmentA"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
    <action
        android:id="@+id/toFragmentB"
        app:destination="@id/fragmentB"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
    <action
        android:id="@+id/toFragmentC"
        app:destination="@id/fragmentC"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
    <action
        android:id="@+id/toFragmentD"
        app:destination="@id/fragmentD"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
    <action
        android:id="@+id/toFragmentE"
        app:destination="@id/fragmentE"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />

What I would like to have

    <action
        android:id="@+id/toFragmentA"
        app:destination="@id/fragmentA"
        app:anim="@anim/slideInOut" />
    <action
        android:id="@+id/toFragmentB"
        app:destination="@id/fragmentB"
        app:anim="@anim/slideInOut" />
    <action
        android:id="@+id/toFragmentC"
        app:destination="@id/fragmentC"
        app:anim="@anim/slideInOut" />
    <action
        android:id="@+id/toFragmentD"
        app:destination="@id/fragmentD"
        app:anim="@anim/slideInOut" />
    <action
        android:id="@+id/toFragmentE"
        app:destination="@id/fragmentE"
        app:anim="@anim/slideInOut" />

With a large nav graph reducing this repeated animation setup would make it much more manageable.

1

There are 1 best solutions below

0
On

One way to do this will be to change the default nav animations and set the animations you want as the default navigations for your fragments. For that purpose, you will have to update the nav options Process:

<androidx.fragment.app.FragmentContainerView
    app:defaultNavHost="true"
    ...
    android:name="androidx.navigation.fragment.NavHostFragment"/>

to:

<androidx.fragment.app.FragmentContainerView
    app:defaultNavHost="true"
    ...
    android:name="your.app.package.fragments.MyNavHostFragment"/>

Now, Create your own nav host fragment to be used instead of the original nav host.

 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 {}

classMyNavHostFragment : 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
                }
            }
        }

}

In here, just change the anim value in navoptions and you are good to go