
Всем привет! С вами снова Максим Бредихин, 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.
Надеюсь, вам было интересно 🙂 До скорых встреч и удачного кодинга!
ссылка на оригинал статьи https://habr.com/ru/company/tinkoff/blog/693794/
Добавить комментарий