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

Android 如何定义导航操作的默认动画?,android,android-animation,android-jetpack,android-architecture-navigation,Android,Android Animation,Android Jetpack,Android Architecture Navigation,我正在使用Android Studio 3.2和14。有了它,您可以像使用意图一样定义过渡动画 但动画设置为导航图中动作的属性,如下所示: <fragment android:id="@+id/startScreenFragment" android:name="com.example.startScreen.StartScreenFragment" android:label="fragment_start_screen" tools:layout="@l

我正在使用Android Studio 3.2和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>

为图形中的所有操作定义此操作会变得单调乏味

是否有办法将一组动画定义为动作的默认动画?

我没有幸为此使用样式。

已定义默认动画(作为最终动画):

  • nav\u default\u enter\u anim

  • nav\u default\u exit\u anim

  • nav\u default\u pop\u enter\u anim

  • nav\u default\u pop\u exit\u anim

要更改此行为,您必须使用custom

因为这是将这些动画指定给的位置

可以使用以下选项分配这些选项:

很可能需要创建一个
DefaultNavFragment
,它扩展了类(那里的文档似乎还没有完成)

因此,您可以将这些
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>

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

  • 导航\默认\输入\动画

  • 导航\默认\退出\动画

  • 导航\默认\弹出\输入\动画

  • 导航\默认\弹出\退出\动画

但是你可以很容易地覆盖它们


只需在你的应用程序模块中创建你自己的四个同名动画资源(澄清一下,其中一个的id是
your.package.name.R.anim.nav\u default\u enter\u anim
),然后编写你想要的动画。

可以使用自定义
androidx.navigation.fragment.Navigator

我将演示如何覆盖
片段
导航。这是我们的自定义导航器。注意
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"
}
}
xml中没有什么特别之处:)



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

我找到了需要扩展
NavHostFragment
的解决方案。它与代码类似,但较少涉及代码。通常需要将所有xml defaultNavHost片段名称从标准更改为:

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

您可以通过传递navOptions在nav graph xml或代码中更改动画。 要禁用默认动画,请使用动画值为0的传递navOptions或传递navigatorExtras(设置共享转换)

测试版本:

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

我还没有看到导航架构组件的完整文档,但我认为必须有一些样式类似的功能,就像我们通常用于其他UI组件一样,用于在动作中制作默认动画。您可以投票选择您可以发布一个示例样式作为主题应用吗?我尝试了你的第二种方法,但不起作用。
NavAction\u enterAnim
导致生成错误,错误:未找到样式属性“attr/NavAction\u enterAnim(又名com.myapp.myapp:attr/NavAction\u enterAnim)”。这对
parent=“Theme.MaterialComponents…”不起作用
我猜他们仍然需要将这些添加到MaterialComponents样式中。对于我来说,material主题或本示例中列出的主题似乎不起作用。您有两个死链接,并且很高兴看到您使用一些代码的第一种方法是什么意思,它就像一个util函数
getNavOptions
,总是返回
NavOptions
?如果是这样的话,那么每次导航时这样做的好处是什么,而不仅仅是在导航图上?有没有办法只使用您在第一个方法中提到的类来覆盖NavOptions?你需要把它和风格结合起来吗?或者第二种方法是扩展NavHostFragment并覆盖什么,以便使
NavOption
默认?只有
NavigationUI
类使用这些资源,其他类如
FragmentNavigator
不使用。因此,此解决方案不适用于按问题要求的操作导航。这应该是公认的答案
 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)
    }
}
<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>
<fragment
    app:defaultNavHost="true"
    ...
    android:name="androidx.navigation.fragment.NavHostFragment"

<fragment
    app:defaultNavHost="true"
    ...
    android:name="your.app.package.fragments.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
                }
            }
        }

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