На просторах интернета много интересных анимированных шаблонов с красивыми пользовательскими интерфейсами мобильных приложений, но не так много примеров с реализацией этих интерфейсов. Несмотря на обилие различных встроенных фреймворков и инструментов в Android SDK, начинающему разработчику зачастую не просто реализовать красивый пользовательский интерфейс, в том числе и при наличии готового шаблона.

В этой статье мы поробуем реализовать пользовательский интерфейс, разработанный дизайнером Иваном Парфеновым для студии PLATES.


Для начала создадим два фрагмента: RecyclerFragment и DetailsFragment.


Android Transition framework?


Android Transition framework работает неплохо, но есть некоторые ньюансы. Во-первых, мы хотим, чтобы у нас все работало хотя бы на API 19, а во-вторых, нам необходимо анимировать несколько пользовательских элементов одновременно и некоторые из них присутствуют только в одном фрагменте. Поэтому анимацию переходного элемента (shared element transition) мы реализуем вручную с использованием ViewPropertyAnimator.

Все по порядку


  1. Вычисляем конечные координаты выбранного элемента из списка (его координаты в DerailsFragment), список — это RecyclerView;
  2. Сохраняем текущие координаты (координаты в RecyclerFragment) и передаем их в DetailsFragment (это нужно для обратной анимации при API < 21);
  3. Создаем копию выбранного из списка элемента;
  4. Делаем выбранный элемент невидимым (не копию, а сам элемент);
  5. Добавляем созданную в п. 3 копию в корневой layout родительского фрагмента, в нашем случае это RecyclerFragment;
  6. Запускаем анимацию остальных элементов интерфейса и перемещаем созданную копию в конечные координаты из п. 1;
  7. Когда анимация закончится, создаем транзакцию и показываем DetailsFragment;
  8. Запускаем анимаци элементов интерфейса в DetailsFragment.

Анимация элементов UI


Для анимации Toolbar мы создадим дополнительную View в RecyclerFragment и разместим ее за экраном сверху. Эта View будет анимироваться в Toolbar контейнер в DetailsFragment (голубой цвет на gif) с использованием ViewPropertyAnimator.

<View
      android:id="@+id/details_toolbar_helper"
      android:layout_width="wrap_content"
      android:layout_height="@dimen/details_toolbar_container_height"
      android:background="@color/colorPrimary"
      app:layout_constraintTop_toTopOf="parent"/>

// In RecyclerFragment
details_toolbar_helper.translationY = -details_toolbar_helper.height

image

Анимация BottomNavigationView и RecyclerView также реализована с помощью ViewPropertyAnimator, ничего сложного (изменение прозрачности и перемещение).

Немножко из Transition framework


Если простыми словами, то android transition framework, когда начинает анимацию переходного элемента, создает копию контента этого переходного элемента (что-то типа print screen), делает из этой копии ImageView, затем добавляет эту картинку в дополнителный слой корневой разметки (overlay layer) в вызываемом фрагменте и запускает анимацию.

Нам android transition framework не совсем подходит, т.к. когда начинается анимация переходного элемента, то все остальные элементы пользовательского интерфейса в фрагменте уничтожаются и мы не можем их анимировать. Т.е. когды мы в RecyclerFragment кликаем на элемент списка для открытия DetailsFragment и стартуем переходную анимацию, то все остальные элементы интерфейса в RecyclerFragment уничтожаются без анимации.

Чтобы получить желаемый результат, мы будем вручную создавать копию выбранного из списка элемента, добавлять его в overlay слой и затем анимировать. Но здесь появляется небольшая проблема, в документации к методу ViewGroupOverlay add(view: View) написано:
If the view has a parent, the view will be removed from that parent before being added to the overlay.

Но для RecyclerView это не работает, выбранный элемент не удаляется из RecyclerView после его добавления в overlay слой.

Вот что получается когда добавляем выбранный элемент в overlay слой:



А нам нужно так:



Поэтому overlay слой мы использовать не будем, а копию будем добавлять сразу в корневой layout. Создадим копию контента выбранного элемента, добавим ее в ImageView и установим координаты:

fun View.copyViewImage(): View {
    val copy = ImageView(context)

    val bitmap = drawToBitmap()
    copy.setImageBitmap(bitmap)

    // В pre-Lollipop при создании копии, тень от card view тоже копируется, и нам не нужна дополнительная card view

    return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        CardView(context).apply {
            cardElevation = resources.getDimension(R.dimen.card_elevation)
            radius = resources.getDimension(R.dimen.card_corner_radius)
            addView(copy)
        }
    } else {
        copy
    }).apply {
        layoutParams = this@copyViewImage.layoutParams
        layoutParams.height = this@copyViewImage.height
        layoutParams.width = this@copyViewImage.width
        x = this@copyViewImage.x
        y = this@copyViewImage.y
    }
}


Зачем создавать копию, если можно просто анимировать непосредственно выбранный из списка элемент?

Потому что сам RecyclerView тоже будет анимироваться и соответсвтенно все его элементы, включая и выбранный, который мы хотим анимировать отдельно.

После этого добавляем копию в корневую разметку и начинаем анимацию.

override fun onClick(view: View) {
    val fragmentTransaction = initFragmentTransaction(view)
    val copy = view.createCopyView()
    root.addView(copy)
    view.visibility = View.INVISIBLE
    startAnimation(copy, fragmentTransaction)
}


И вот, что у нас получилось:



Финишная прямая


Анимация на gif выше происходит в RecyclerFragment, а после ее завершения нам необходимо показать DetailsFragment.

.withEndAction {
    fragmentTransaction?.commitAllowingStateLoss()
}

Почему мы используем commitAllowingStateLoss?

Если его не использовать и в момент анимации будет, например смена ориентации экрана, то мы получим IllegalStateExсeption. Вот здесь хорошо про это написано.

Далее запускаем анимацию необходимых элементов пользовательского интерфейса в DetailsFragment.

Запустим все вместе




Не совсем так, как на оригинале, но выглядит похоже.

Примеры


Исходный код доступен на GitHub, также статья доступна на английском языке.

Спасибо за внимание!

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


  1. eskander_service
    16.09.2018 20:57
    -1

    Доброго времени суток, будьте добры, добавляйте тег «Kotlin», что бы не тратить время. Спасибо.