如何为导航操作定义默认动画?

58

我正在使用Android Studio 3.2 Canary 14和导航架构组件。使用它,您基本上可以像使用意图时一样定义转换动画。

但是,动画是作为导航图中操作的属性设置的,例如:

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

对于图中的所有操作,这种定义变得很繁琐!

有没有一种方法可以在动作上定义一组默认的动画?

我尝试使用样式来实现这一点,但没有成功。


我还没有看到有关导航架构组件的完整文档,但我认为必须像其他UI组件一样使用某些样式功能来制作操作中的默认动画。 - Aseem Sharma
你可以投票支持相应的问题议题 - gmk57
6个回答

52

R.anim 定义了默认的动画效果(作为最终结果):

  • nav_default_enter_anim

  • nav_default_exit_anim

  • nav_default_pop_enter_anim

  • nav_default_pop_exit_anim

要更改此行为,您需要使用自定义NavOptions

因为这是将这些动画分配给NavAction的位置。

您可以使用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;
}

大多数情况下,您需要创建一个 DefaultNavFragment ,它扩展了类androidx.navigation.fragment(那里的文档似乎还没有完成)。

因此,您可以像这样将这些 NavOptions 传递给 NavHostFragment

NavHostFragment.findNavController(this).navigate(R.id.your_action_id, null, getNavOptions());

或者... 当查看该包的attrs.xml时;这些动画是可以进行样式化的:是的

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

这意味着,可以定义相应的样式 - 并将其定义为主题的一部分...

可以在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>

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

2
你能发布一个样式示例以应用为主题吗?我尝试了你的第二种方法,但它不起作用。 - Hafez Divandari
10
"NavAction_enterAnim" 在构建时出现错误,错误信息为:找不到样式属性 'attr/NavAction_enterAnim'(即 com.myapp.myapp:attr/NavAction_enterAnim)。请检查代码。 - Hafez Divandari
4
使用 parent="Theme.MaterialComponents..." 将不能正常工作。我猜想他们需要将这些添加到 MaterialComponents 样式中。 - Peter Keefe
3
对我来说,使用Material主题或此示例中列出的主题似乎无法正常工作。 - William Reed
4
注意,这将覆盖XML中指定的NavOptions,这意味着您将丢失诸如singleTop、popUpTo和默认参数等属性。 - Risal Fajar Amiyardi
显示剩余11条评论

12

我发现一种解决方案,需要扩展NavHostFragment。这与Link182类似,但涉及的代码较少。通常需要更改所有xml defaultNavHost片段名称的标准:

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

至:

<fragment
    app:defaultNavHost="true"
    ...
    android:name="your.app.package.fragments.NavHostFragmentWithDefaultAnimations"

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

}

您可以通过在导航图XML或代码中传递navOptions来更改动画效果。要禁用默认动画,请使用值为0的anim值传递navOptions,或者传递navigatorExtras(设置共享转场)。

已测试版本:

implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"

对于版本2.5.2

fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?) 

也必须被覆盖。


3
太好了!如果你能用这个代码创建一个小型库并将其发布在jitpack.io上,那就太棒了。 - binarynoise
3
我的意思是,这种事情会在本能层面上伤害我。别误解我,这是一个实用的解决方案,但它只是展示了 Navigation Ui 原始作者的无能,他们设计原始库时竟然要我们进行如此程度的自定义,以解决一些琐碎的全局动画问题。 - YaMiN
对于较新版本的导航系统,请确保复制功能已更新,包括所有参数。 - undefined

2

这是我的解决方案,在我的应用程序中运行良好。

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);
    }

1

我已经创建了扩展,并在需要时调用它,而不是调用navigation

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())

我只想知道如何调用它!请为我解释NavDirections。 - AllanRibas
@AllanRibas “如何调用?” 答案在这里。使用 findNavController().navigateWithDefaultAnimation(HomeFragmentDirections.homeToProfile()) 而不是仅仅使用 navigate。HomeFragmentDirectons 是由 SafeArgs 生成的方向类,它包含了目标(例如 homeToProfile)。 - Suryavel TR

-1

如上所述,R.anim定义了默认的动画:

  • nav_default_enter_anim

  • nav_default_exit_anim

  • nav_default_pop_enter_anim

  • nav_default_pop_exit_anim

但是您可以轻松地覆盖它们。

只需在您的应用程序模块中创建自己的四个动画资源,并使用相同的名称命名它们(仅为澄清起见,其中一个的ID是your.package.name.R.anim.nav_default_enter_anim),然后编写您想要的动画即可。


5
只有 NavigationUI 类使用这些资源,其他类如 FragmentNavigator 不使用。因此,这个解决方案无法满足问题中请求的使用动作导航的要求。 - Hafez Divandari

-1

使用自定义androidx.navigation.fragment.Navigator是可能的。

我将演示如何覆盖fragment导航。这是我们的自定义导航器。请注意setAnimations()方法。

@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"
}
}

在下一步中,我们需要将导航器添加到NavController中。以下是如何进行设置的示例:
 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)
    }
}

在 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>

现在图中的每个片段都将具有alpha转换


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接