Как определить анимацию по умолчанию для действий навигации?

Я использую 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>

Это утомительно определять для всех действий на графике!

Есть ли способ определить набор анимаций по умолчанию для действий?

Мне не повезло с использованием стилей для этого.


person Smedegaard    schedule 23.05.2018    source источник
comment
Я не видел полный документ компонента архитектуры навигации, но я думаю, что должна быть какая-то функция, похожая на стиль, которую мы обычно используем для других компонентов пользовательского интерфейса, для создания анимации по умолчанию в действиях.   -  person Aseem Sharma    schedule 23.05.2018
comment
Вы можете проголосовать за соответствующее проблемы   -  person gmk57    schedule 12.02.2021


Ответы (4)


В 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. фрагмент (документация там еще не завершена).

Таким образом, вы можете передать эти 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>
person Martin Zeitler    schedule 19.09.2018
comment
Можете ли вы опубликовать образец стиля для применения в качестве темы? Я попробовал ваш второй подход, но он не работает. - person Hafez Divandari; 23.09.2018
comment
@HafezDivandari см. developer.android.com/guide/topics/ui /look-and-feel/themes — вам нужно определить эти стили и передать ресурс (анимацию); Я показал только эту часть attrs.xml, чтобы сообщить имя стиля и его стили (тема также должна быть назначена приложению или активности). - person Martin Zeitler; 23.09.2018
comment
@HafezDivandari также добавил пример; просто не уверен в пространстве имен parent; думаю, что это не @android:style/, потому что это стили библиотеки, а не стили фреймворка. - person Martin Zeitler; 23.09.2018
comment
ошибка: Не удается разрешить символ «NavAction», я применил эту тему (без paret=NavAction) к тегу навигации, но она не работает. - person Hafez Divandari; 23.09.2018
comment
@HafezDivandari здесь они также объявлены: developer.android.com/ reference/androidx/navigation/ui/ — имя может быть NavAction_popEnterAnim без родителя. - person Martin Zeitler; 23.09.2018
comment
NavAction_enterAnim вызывает ошибку при сборке, ошибка: атрибут стиля 'attr/NavAction_enterAnim (он же com.myapp.myapp:attr/NavAction_enterAnim)' не найден. - person Hafez Divandari; 28.09.2018
comment
Это не будет работать с parent="Theme.MaterialComponents..." Я предполагаю, что им все еще нужно добавить их в стили MaterialComponents. - person Peter Keefe; 07.02.2019
comment
похоже, у меня не работает с материальной темой или темой, указанной в этом примере - person William Reed; 05.06.2019
comment
У вас есть 2 неработающие ссылки, а также было бы неплохо посмотреть, что вы имеете в виду под своим первым подходом к некоторому коду, это похоже на функцию утилиты getNavOptions , которая всегда возвращает этот NavOptions? Если это так, то какая польза делать это каждый раз, когда вы перемещаетесь, вместо того, чтобы просто смотреть на свой навигационный график? Есть ли способ перезаписать NavOptions, используя только классы, которые вы только что упомянули в своем первом методе? Нужно ли сочетать это со стилями? Или второй подход состоит в том, чтобы расширить NavHostFragment и перезаписать что, чтобы иметь это NavOption по умолчанию? - person desgraci; 11.06.2019
comment
для тех, кто хочет использовать их в xml ?attr/enterAnim ?attr/exitAnim ?attr/popEnterAnim ?attr/popExitAnim - person vahid; 30.09.2020
comment
Настройка атрибутов темы у меня не сработала, и похоже, что это не сработает, глядя на исходники версии 2.3.1 библиотеки навигации androidx.tech/artifacts/navigation/navigation-runtime/ - person fada21; 17.01.2021
comment
@fada21 fada21 Them происходит из контекста Activity; а инфлятору все равно, как он оформлен. - person Martin Zeitler; 17.01.2021
comment
@MartinZeitler inflateAction не может знать о теме, так как получает атрибуты от final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction);, который не заботится о стиле или теме. Resources.Theme#obtainStyledAttributes делает. - person fada21; 17.01.2021
comment
ВНИМАНИЕ, это переопределит параметры NavOptions, указанные в XML, что означает, что вы потеряете такие свойства, как singleTop, popUpTo и аргументы по умолчанию. - person Risal Fajar Amiyardi; 01.06.2021
comment
Стилизация чего-либо должна переопределять только стилизованные значения; убедитесь, что у вас есть родительская тема... если нет, она может выглядеть так, как если бы, но этих свойств может не быть с самого начала. - person Martin Zeitler; 02.06.2021

Я нашел решение, которое требует расширения NavHostFragment. Он похож на Link182, но требует меньше кода. Чаще всего потребуется изменить все имена фрагментов xml defaultNavHost со стандартных:

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

to:

<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. Чтобы отключить анимацию по умолчанию, передайте navOptions со значениями анимации 0 или передайте navigatorExtras (настройка общих переходов).

Проверено для версии:

implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"
person fada21    schedule 16.01.2021
comment
Это должен быть принятый ответ - person Risal Fajar Amiyardi; 20.02.2021
comment
Это круто! Было бы здорово, если бы вы создали небольшую библиотеку и опубликовали ее на jitpack.io. - person binarynoise; 01.08.2021

Это возможно с пользовательским 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>

Теперь каждый фрагмент графика будет иметь альфа-переходы

person Link182    schedule 11.12.2019

Как уже говорилось, в R.anim определены анимации по умолчанию:

  • nav_default_enter_anim

  • nav_default_exit_anim

  • nav_default_pop_enter_anim

  • nav_default_pop_exit_anim

Но вы можете легко переопределить их.

Просто создайте свои собственные четыре анимационных ресурса с одинаковыми именами в модуле вашего приложения (для уточнения, идентификатор одного из них — your.package.name.R.anim.nav_default_enter_anim) и напишите, какую анимацию вы хотите.

person nbaroz    schedule 26.09.2018
comment
Только NavigationUI класс использует эти ресурсы, другие классы, такие как FragmentNavigator, не используют их. Таким образом, это решение не работает при навигации с помощью действий, запрашиваемых в вопросе. - person Hafez Divandari; 28.09.2018