Я использую последний компонент навигации (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, но безрезультатно.