Из Dribbble в Android Motion



    На просторах интернета много интересных анимированных шаблонов с красивыми пользовательскими интерфейсами мобильных приложений, но не так много примеров с реализацией этих интерфейсов. Несмотря на обилие различных встроенных фреймворков и инструментов в 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, также статья доступна на английском языке.

    Спасибо за внимание!
    • +21
    • 3,1k
    • 1
    Поделиться публикацией

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

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

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое