Неочевидное про Fragment API. Часть 4. Анимации и меню

от автора

Всем привет! С вами снова Максим Бредихин, Android-разработчик в Тинькофф. Мы добрались до заключительной части серии про интересные моменты из Fragment API. Занимайте лучшие места, мы начинаем!

Анимации и переходы

Мы можем определить простые анимации для переходов между фрагментами в папке 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:

  1. MenuHost.addMenuProvider(MenuProvider) — нужно руками удалить MenuProvider.

  2. MenuHost.addMenuProvider(MenuProvider, LifecycleOwner) — MenuProvider удалится в состоянии DESTROYED.

  3. 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *