Всем привет! С вами снова Максим Бредихин, Android-разработчик в Тинькофф. Мы добрались до заключительной части серии про интересные моменты из Fragment API. Занимайте лучшие места, мы начинаем!
Часть 4. Анимации и меню (вы находитесь здесь)
Анимации и переходы
Мы можем определить простые анимации для переходов между фрагментами в папке res/anim. Но если мы хотим управлять любыми атрибутами вьюшки нашего фрагмента, то должны указать анимации в папке res/animator. Более того, можем их спокойно комбинировать в рамках транзакции.
fragmentManager.commit {
setReorderingAllowed(true)
// Должны быть указаны до add/replace, иначе они проигнорируются
setCustomAnimations(
R.animator.anim_enter, // InnerFragment появляется на экране
R.anim.anim_exit, // OuterFragment уходит с экрана
R.anim.anim_pop_enter, // OuterFragment возвращается на экран
R.animator.anim_pop_exit // InnerFragment уходит с экрана
)
replace<InnerFragment>(R.id.container)
addToBackStack(null)
}
Эти анимации автоматически применяются ко всем последующим транзакциям с использованием этого fragmentManager
. А теперь повертим нашим фрагментом:
<!-- animator/anim_enter.xml -->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="400"
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotation" />
<!-- anim/anim_exit.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="400"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<!-- anim/anim_pop_enter.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="400"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<!-- animator/anim_pop_exit.xml -->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="400"
android:valueFrom="180"
android:valueTo="360"
android:propertyName="rotation" />
Вот что получилось в итоге:
Если не хочется прописывать каждую анимацию, можно использовать Transition — например, заготовленный Fade(). Он указывается в InnerFragment
и затирает анимацию в транзакции, если она была указана.
// InnerFragment.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Анимация при переходе на экран
enterTransition = Fade()
// Анимация при выходе с экрана через fragmentManager.popBackStack()
// Eсли не указана, будет использована enterTransition
exitTransition = Fade()
// Анимация при выходе с экрана не через fragmentManager.popBackStack()
// Например, через replace()
// Eсли не указана, будет использована enterTransition
returnTransition = Fade()
// Анимация при возврате на экран через fragmentManager.popBackStack()
// Eсли не указана, будет использована enterTransition
reenterTransition = Fade()
}
Но это еще не верх возможностей анимации во Fragment API. Следующая ступень развития — shared element transitions, с помощью которых можно получить подобный переход.
Для создания такой анимации воспользуемся методом FragmentTransaction.addSharedElement(View, String)
и стандартным переходом ChangeBounds().
Сначала нужно указать у view-элементов, которые хотим анимировать, уникальные в рамках разметки transitionName
. Сделать это можно через xml или в коде. В InnerFragment
указываем анимации:
<!--- fragment_outer_layout.xml -->
<TextView
android:id="@+id/textViewStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Outer Fragment"
android:transitionName="text_start" />
<!--- fragment_inner_layout.xml -->
<TextView
android:id="@+id/textViewDestination"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Inner Fragment"
android:transitionName="text_destination" />
// OuterFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ViewCompat.setTransitionName(imageViewStart, "image_start")
}
// InnerFragment.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Анимация при открытии фрагмента
sharedElementEnterTransition = ChangeBounds()
// Анимации при закрытии фрагмента
// Если не указать, будет использована sharedElementEnterTransition
sharedElementReturnTransition = ChangeBounds()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ViewCompat.setTransitionName(imageViewDestination, "image_destination")
}
А теперь вызываем транзакцию:
// OuterFragment.kt
parentFragmentManager.commit {
setReorderingAllowed(true)
addSharedElement(imageViewStart, "image_destination")
addSharedElement(textViewStart, "text_destination")
replace<InnerFragment>(R.id.container)
addToBackStack(null)
}
Если все вьюшки отрисовываются синхронно, то вот так просто мы можем получить анимацию, показанную выше.
Если же мы используем shared element transition с RecyclerView, то нужно помнить, что она отрисовывает свои айтемы после того, как отрисуется разметка экрана. Получается, анимацию перехода нужно приостановить до готовности к отрисовки элементов списка.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Приостанавливаем переход
postponeEnterTransition()
// Ждем, когда все загрузится
viewModel.data.observe(viewLifecycleOwner) {
// Передаем данные в адаптер RecyclerView
adapter.setData(it)
// Ждем, когда все элементы будут готовы к отрисовке, и запускаем анимацию
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
Важно! Метод postponeEnterTransition() требует использования FragmentTransaction.setReorderingAllowed(true).
Аналогичная логика будет при использовании данных из сети, которые нужно подгрузить на новый экран.
Готовим меню правильно
С версии Fragments 1.5.0 метод setHasOptionsMenu(true)
был помечен как deprecated
. Он использовался, чтобы сказать системе, что данный фрагмент хочет получать относящиеся к меню в AppBar родительской Activity колбеки: onCreateOptionsMenu()
, onPrepareOptionsMenu()
и onOptionsItemSelected()
.
Вместо него теперь рекомендуется использовать MenuProvider. Если мы используем несколько MenuProvider, то вызываться они будут по мере добавления, начиная с Activity.
Есть три перегрузки метода addMenuProvider()
, чтобы добавить MenuProvider:
MenuHost.addMenuProvider(MenuProvider)
— нужно руками удалить MenuProvider.MenuHost.addMenuProvider(MenuProvider, LifecycleOwner)
— MenuProvider удалится в состоянии DESTROYED.MenuHost.addMenuProvider(MenuProvider, LifecycleOwner, Lifecycle.State)
— MenuProvider добавляется в указанном состоянии ЖЦ и удаляется при выходе из этого состояния либо при достижении DESTROYED.
class ExampleFragment : Fragment(R.layout.fragment_example) {
private val menuHost: MenuHost get() = requireActivity()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
menuHost.addMenuProvider(object : MenuProvider { // Добавляем MenuProvider
override fun onPrepareMenu(menu: Menu) // Вызывается перед отрисовкой меню
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
// Надуваем fragment_menu и мержим с прошлым menu
menuInflater.inflate(R.menu.fragment_menu, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
// Пользователь кликнул на элемент меню
// return true — не нужно передавать нажатие другому провайдеру
// return false — передаем нажатие следующему провайдеру
return false
}
override fun onMenuClosed(menu: Menu) // Меню закрыто
}, viewLifecycleOwner)
}
}
Несказанное про Fragment-ktx
Фрагмент — это только ui-слой, всю логику мы должны прятать во ViewModel или куда-нибудь за нее в зависимости от архитектуры. Для быстрого создания и работы со ViewModel нам приготовили пару ленивых расширений-делегатов.
Доступ ко ViewModel родительской Activity, которую можно использовать для шаринга данных между несколькими фрагментами:
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM>
// Example
class ExampleFragment : Fragment() {
private val viewModel: ExampleViewModel by activityViewModels()
}
Доступ ко ViewModel фрагмента:
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM>
// Example
class ExampleFragment : Fragment() {
private val viewModel: ExampleViewModel by viewModels()
}
Заключение
Вот и подошла к концу моя серия статей. Я собрал самые интересные и неочевидные особенности Fragment API, о которых, возможно, не все знали.
Простые и очевидные моменты подробно описаны в документации. А более сложные темы, к примеру backstack, в документации, как правило, описаны поверхностно.
Когда я собирал материал, хотел помочь сделать код чище и показать, как можно:
— избавиться от бойлерплейта;
— обеспечить общение между фрагментами без создания костылей;
— получить больший контроль над жизненным циклом фрагментов;
— покорить backstack.
Чтобы выявить все правила и особенности успешной работы с возможностями, о которых я рассказал, мне пришлось залезть далеко в глубины Fragment API.
Надеюсь, вам было интересно :) До скорых встреч и удачного кодинга!