Компонент навигации - переход общего элемента вообще не работает

Я использую последний компонент навигации (2.2.0-alpha01) в своем проекте и столкнулся с проблемой, которую не могу решить.

У меня сплеш-анимация — ничего серьезного, кастомный фон, растягивающийся на весь экран, и логотип в самой середине, с помощью ConstraintLayout. Во время начальной синхронизации я анимирую собственный анимированный объект VectorDrawable (назовем его @drawable/logo_animated), который использует общий @drawable/logo в качестве источника и применяет анимацию к своим группам.

Чтобы правильно рассчитать время анимации, я создал следующую вспомогательную функцию:

fun ImageView.setRepeatingAnimatedVector(
    @DrawableRes animationRes: Int,
    delayMs: Long = 0,
    startDelayMs: Long = 0,
    shouldRunOptional: () -> Boolean = { false },
    optionalRunnable: () -> Unit = {}
) {
    val anim = AnimatedVectorDrawableCompat.create(context, animationRes)?.apply {
        registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
            override fun onAnimationEnd(drawable: Drawable?) {
                [email protected]({ if (shouldRunOptional()) optionalRunnable() else start() }, delayMs)
            }
        })
    }
    setImageDrawable(anim)
    postDelayed({ anim?.start() }, startDelayMs)
}

Он принимает AnimatedVectorDrawable в качестве входных данных и применяет его к ImageView. По окончании цикла анимации запускается проверка в виде лямбды (shouldRunOptional). Если он возвращает true, запускается лямбда optionalRunnable, в противном случае повторяется анимация.

При этом я могу дождаться завершения синхронизации ViewModel, а затем дождаться окончания анимации, чтобы перемещаться между фрагментами без каких-либо странностей. Сама анимация короткая (~900 мс), поэтому пользователь будет задерживаться максимум на секунду.

Я также использую пользовательскую композицию NavigationManager для навигации. Сам менеджер представляет собой интерфейс (INavigationManager) общих вызовов (таких как splashToLanding() или openDetail(id: UUID)), который внедряется в ViewModels, с дополнительным интерфейсом, который заботится о конкретных битах NavComponent:

IFragmentNavigator.kt

interface IFragmentNavigator {
    val command: SingleLiveEvent<NavigationCommand>

    var splashLandingExtras: Navigator.Extras?

    fun setSplashLandingTransition(extras: Navigator.Extras) {
        splashLandingExtras = extras
    }

    fun back() {
        navigate(NavigationCommand.Back)
    }

    fun navigate(direction: NavDirections) {
        navigate(NavigationCommand.To(direction))
    }

    fun navigate(navCommand: NavigationCommand) {
        command.postValue(navCommand)
    }
}

Реализация просто заботится об инициализации свойств, а затем используется следующим образом:

class FragmentNavigationManager: 
    INavigationManager, IFragmentNavigator by FragmentNavigator() { [...] }

Свойство command этого интерфейса затем используется во фрагментах через Observers:

open val navigationObserver = Observer<NavigationCommand> {
        when(it) {
            is NavigationCommand.To -> findNavController().navigate(it.directions)
            is NavigationCommand.Back -> findNavController().popBackStack()
            is NavigationCommand.BackTo -> findNavController().popBackStack(it.destinationId, false)
            is NavigationCommand.ToRoot -> TODO()
        }
    }

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    navigator.command.observe(this, navigationObserver)
}

Экземпляр Directions, созданный в FragmentNavigationManager, напрямую используется NavController. Я обязательно добавил поле Extras FragmentNavigator к фактическому вызову навигации:

    override fun splashToLanding() {
        navigate(
            NavigationCommand.To(
                SplashFragmentDirections.actionSplashFragmentToLandingFragment(),
                null, null, splashLandingExtras
            )
        )
    }

И, конечно же, во SplashFragment я назначаю соответствующее представление имени перехода для splashLandingExtras:

navigator.splashLandingExtras = FragmentNavigatorExtras(binding.logo to "logo")

В методе LandingFragment onCreate я настроил анимацию входа и выхода:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        TransitionInflater.from(context).inflateTransition(android.R.transition.move).let {transition ->
            sharedElementEnterTransition = transition
            sharedElementReturnTransition = transition
        }
    }

Макеты следующие:

всплеск.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.felcana.app.viewmodel.SplashViewModel" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <include layout="@layout/background" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true">

            <ImageView
                android:id="@+id/logo"
                style="?attr/logoStyle"
                android:transitionName="logo"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>


    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

лендинг.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.my.app.viewmodel.LandingViewModel" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <include layout="@layout/background" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true">

            <ImageView
                android:id="@+id/logo"
                style="?attr/logoStyle"
                android:layout_marginBottom="24dp"
                android:transitionName="logo"
                app:layout_constraintBottom_toTopOf="@id/button_register"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/button_register"
                style="?attr/flatWhiteButtonStyle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="24dp"
                android:layout_marginEnd="24dp"
                android:layout_marginBottom="8dp"
                android:onClick="@{() -> viewModel.goToRegister()}"
                android:text="@string/button_register"
                app:layout_constraintBottom_toTopOf="@id/button_login"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/button_login"
                style="?attr/borderlessWhiteButtonStyle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:onClick="@{() -> viewModel.goToLogin()}"
                android:text="@string/button_login"
                app:layout_constraintBottom_toTopOf="@id/disclaimer"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />

            <TextView
                android:id="@+id/disclaimer"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="24dp"
                android:layout_marginEnd="24dp"
                android:layout_marginBottom="32dp"
                android:maxLines="2"
                android:textAlignment="center"
                android:textColor="@color/app_white"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                tools:text="@tools:sample/lorem/random" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

Почему-то анимация вообще не воспроизводится - ImageView просто прыгает без какого-либо перехода на новую позицию.

Что здесь не так? Согласно документации, это должно работать. Я пытался вернуться к более стабильным версиям библиотеки NavComponent, но безрезультатно.




Ответы (1)


Вы пытались отложить переход ввода внутри LandingFragment::onViewCreated и вручную установить имя перехода для представления?

Что-то вроде этого:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  // Pause the enter transition
  postponeEnterTransition()

  // Manually apply the transitionName
  logo.transitionName = "logo"

  // Resume the transition
  startPostponedEnterTransition()

  super.onViewCreated(view, savedInstanceState)
}
person Harry    schedule 02.12.2019