Всем привет! С вами снова Максим Бредихин, 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. 

Надеюсь, вам было интересно :) До скорых встреч и удачного кодинга! 

Комментарии (0)