- PVSM.RU - https://www.pvsm.ru -
На просторах интернета много интересных анимированных шаблонов с красивыми пользовательскими интерфейсами мобильных приложений, но не так много примеров с реализацией этих интерфейсов. Несмотря на обилие различных встроенных фреймворков и инструментов в Android SDK, начинающему разработчику зачастую не просто реализовать красивый пользовательский интерфейс, в том числе и при наличии готового шаблона.
В этой статье мы поробуем реализовать пользовательский интерфейс, разработанный Иваном Парфеновым [1] для студии PLΛTES [2].
Для начала создадим два фрагмента: RecyclerFragment
и DetailsFragment
.
Android Transition framework работает неплохо, но есть некоторые ньюансы. Во-первых, мы хотим, чтобы у нас все работало хотя бы на API 19, а во-вторых, нам необходимо анимировать несколько пользовательских элементов одновременно и некоторые из них присутствуют только в одном фрагменте. Поэтому анимацию переходного элемента (shared element transition) мы реализуем вручную с использованием ViewPropertyAnimator
[3].
DerailsFragment
), список — это RecyclerView
;RecyclerFragment
) и передаем их в DetailsFragment
(это нужно для обратной анимации при API < 21);RecyclerFragment
;DetailsFragment
;DetailsFragment
.
Для анимации 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
Анимация BottomNavigationView
и RecyclerView
также реализована с помощью ViewPropertyAnimator
, ничего сложного (изменение прозрачности и перемещение).
Если простыми словами, то 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
. Вот здесь [4] хорошо про это написано.
Далее запускаем анимацию необходимых элементов пользовательского интерфейса в DetailsFragment
.
Не совсем так, как на оригинале, но выглядит похоже.
Исходный код доступен на GitHub [5], пример приложения с похожим дизайном можно скачать из Google Play [6], также статья доступна на английском языке [7]. Спасибо за внимание!
Автор: Александр
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ui/292680
Ссылки в тексте:
[1] Иваном Парфеновым: https://dribbble.com/parfenoff
[2] PLΛTES: https://dribbble.com/plates
[3] ViewPropertyAnimator
: https://developer.android.com/reference/android/view/ViewPropertyAnimator
[4] Вот здесь: https://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
[5] GitHub: https://github.com/bitvale/DroidMotion
[6] Google Play: https://play.google.com/store/apps/details?id=com.skycodetech.codingquiz
[7] английском языке: https://android.jlelse.eu/from-dribbble-template-to-android-motion-74fe03282c88
[8] Источник: https://habr.com/post/423235/?utm_source=habrahabr&utm_medium=rss&utm_campaign=423235
Нажмите здесь для печати.