Transitions API: делаем анимацию для Android приложений
При всей важности и распространенности анимации в мобильной разработке, программисты отмечают, что в Android-приложениях её создание всегда было непростой задачей. Однако с 19 API ситуация изменилась, и теперь работать с анимациями стало гораздо проще.
Инструменты, о которых мы расскажем сегодня, — это удобное решение для самых разных анимаций: вместо отдельных экранов программист анимирует так называемые сцены, а переходы между ними генерируются автоматически при помощи Transition API — и это лишь верхушка айсберга возможностей. И если вы еще не используете эту технологию на своих проектах — самое время попробовать!
Примечание: данная статья была написана в мае 2015 года, поэтому некоторые моменты могут оказаться немного устаревшими. Однако общий подход к анимациям в Android почти не изменился, поэтому статья все еще является актуальной.
Transitions API: как это работает?
Уже в Android 4.0 существовал ранний вариант решения проблемы с анимацией — флаг animateLayoutChange для ViewGroup. Однако этот инструмент был недостаточно гибким и не мог обеспечить разработчику полный контроль над переходами (Transitions). В Android 4.4 KitKat и выше были реализованы Transitions API. Поскольку Transitions API также есть в саппорт-библиотеке, теперь с их помощью можно упростить работу с анимацией практически на любом девайсе под Android.
Именно в KitKat Transition API появляется такие понятия как сцена — Scene, и Transition — переход между сценами. Для определения корневого layout вводится понятие Scene root, внутри которого и происходит изменение сцен. При этом сама сцена по сути является враппером над ViewGroup, описывающим своё состояние и состояние объектов View в нем.
Сам Transition — механизм, который позволяет считывать параметры View, изменяющиеся от сцены к сцене, и генерировать анимации для создания плавных переходов между ними.
Transition Framework предоставляет следующие возможности для создания анимаций:
- Group-level animations: возможность анимировать целые иерархии объектов View. Разработчик указывает ViewGroup, которую нужно санимировать, и анимации автоматически применяются к каждому её элементу.
- Transition-based animation: анимации, основанные на переходах.
- Built-in animations: простые встроенные анимации, которые генерируются автоматически, такие как растворение, затемнение, изменение размера, движение и т.д.
- Resource file support: поддержка файлов ресурсов, в которых может быть записано, что и как анимировать. Таким образом, необязательно прописывать все анимации в коде.
- Lifecycle callbacks: предоставляет все необходимые методы контроля за воспроизведением анимации.
При всех своих достоинствах данный метод создания анимаций имеет и некоторые ограничения:
- Может давать сбои, если применяется к наиболее сложным SurfaceView и TextureView, работающим не в UI потоке, что ведет к рассинхронизации анимации.
- Плохо работают с AdapterView, такими как ListView, когда в них необходимо анимировать отдельные элементы.
- Периодически возникают проблемы с синхронизацией при попытке изменить размер TextView: шрифт может отобразиться в следующей сцене до того, как закончилось изменение остальных объектов.
Однако эти ограничения вряд ли можно назвать существенными: на практике ситуации, когда нужно применить анимацию, к примеру, к SurfaceView, встречаются крайне редко.
Рассмотрим примеры работы с Transition Framework
Создание сцены из ресурса:
res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/master_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/title" ... android:text="Title"/> <FrameLayout android:id="@+id/scene_root" android:layout_width="match_parent" android:layout_height="0dp" android:weight="1"> <include layout="@layout/scene_first" /> </FrameLayout> </LinearLayout>
res/layout/scene_first.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scene_container" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/text_view1 ... android:text="Text Line 1" /> <TextView android:id="@+id/text_view2 ... android:text="Text Line 2" /> </RelativeLayout>
res/layout/scene_second.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scene_container" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/text_view2 ... android:text="Text Line 2" /> <TextView android:id="@+id/text_view1 ... android:text="Text Line 1" /> </RelativeLayout>
lateinit var firstScene: Scene lateinit var secondScene: Scene // Получаем корневой элемент для цен в нашем приложении sceneRoot = findViewById<ViewGroup>(R.id.scene_root) // Получаем сцены firstScene = Scene.getSceneForLayout(sceneRoot, R.layout.scene_first, this) secondScene = Scene.getSceneForLayout(sceneRoot, R.layout.scene_second, this)
Создание сцены из кода:
// Получаем корневой элемент для цен в нашем приложении sceneRoot = findViewById<ViewGroup>(R.id.scene_root) // Получаем контейнер для добавления сцены viewHierarchy = findViewById<ViewGroup>(R.id.scene_container) // Создаем сцену val scene = Scene(sceneRoot, viewHierarchy)
Сами Transitions также создаются на базе ресурса:
res/transition/fade_transition.xml
<fade xmlns:android="http://schemas.android.com/apk/res/android" />
val fadeTransition: Transition = TransitionInflater.from(this) .inflateTransition(R.transition.fade_transition)
Или кода:
val fadeTransition: Transition = Fade()
Есть возможность создавать целые сеты анимаций, например, одновременное движение, изменение размера и затемнение
В ресурсе:
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:transitionOrdering="sequential"> <fade android:fadingMode="fade_out" /> <changeBounds /> <fade android:fadingMode="fade_in" /> </transitionSet>
В коде:
val set = TransitionSet() set.addTransition(Fade()) .addTransition(ChangeBounds()) .addTransition(AutoTransition())
Еще одна возможность — применить анимацию не ко всей сцене, а к отдельному объекту View
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"> <changeBounds /> <fade android:fadingMode="fade_in" /> <targets> <target android:targetId="@id/transition_title" /> </targets> </fade> </transitionSet>
Transition manager создается одной строчкой кода. Он существует для того, чтобы прописать все сцены и переходы в одном месте. Это позволяет существенно сократить объем работ и упростить дальнейший контроль над анимацией:
res/transition/transition_manager.xml
<transitionManager xmlns:app="http://schemas.android.com/apk/res-auto"> <transition app:fromScene="@layout/scene_reg1" app:toScene="@layout/scene_reg2" app:transition="@transition/trans_reg1_to_reg2" /> <transition app:fromScene="@layout/scene_reg2" app:toScene="@layout/scene_reg3" app:transition="@transition/trans_reg2_to_reg3" /> ... </transitionManager>
val transitionManager = TransitionInflater.from(context) .inflateTransitionManager(R.transition.transition_manager, sceneRoot)
Как запускать сцены? Очень просто!
С кастомными Transitions:
transitionManager.transitionTo(scene)
или
transitionManager.go(scene, fadeTransition)
C Transitions по умолчанию:
transitionManager.go(scene)
Или без Transitions:
scene.enter()
Использовать переходы можно и отдельно, не создавая сцены
res/layout/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainLayout" android:layout_width="match_parent" android:layout_height="match_parent" > <EditText android:id="@+id/inputText" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_width="match_parent" android:layout_height="wrap_content" /> ... </RelativeLayout>
MainActivity.java
private lateinit var labelText: TextView private lateinit var fade: Fade private lateinit var rootView: ViewGroup // Устанавливаем разметку setContentView(R.layout.activity_main) // Создаем новый TextView и устанавливаем некоторые свойства labelText = TextView() labelText.setText("Label").setId("1") // Получаем корневой элемент rootView = findViewById<ViewGroup>(R.id.mainLayout) fade = Fade(IN) // Начинаем записывать изменения TransitionManager.beginDelayedTransition(rootView, fade) // Добавляем новый TextView rootView.addView(labelText) // Когда система перерисует экран, добавив это изменение, // фреймворк выполнит анимацию добавления появлением
При помощи логичного и интуитивно понятного интерфейса TransitionListener вы можете контролировать каждый шаг любой анимации:
interface TransitionListener { fun onTransitionStart(transition: Transition) fun onTransitionEnd(transition: Transition) fun onTransitionCancel(transition: Transition) fun onTransitionPause(transition: Transition) fun onTransitionResume(transition: Transition) }
Помимо встроенных, вы можете создавать и собственные анимации. Например, так можно изменить цвет фона View:
class ChangeColor extends Transition { companion object { const val PROPNAME_BACKGROUND = "customtransition:change_color:background" } private fun captureValues(values: TransitionValues) { values.values.put(PROPNAME_BACKGROUND, values.view.getBackground()); } override fun captureStartValues(transitionValues: TransitionValues) { captureValues(transitionValues) } override fun captureEndValues(transitionValues: TransitionValues) { captureValues(transitionValues) } override fun createAnimator( ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues ): Animator { if (startValues == null || endValues == null) { return null } val view = endValues.view val startBackground = startValues.values.get(PROPNAME_BACKGROUND) as Drawable val endBackground = endValues.values.get(PROPNAME_BACKGROUND) as Drawable val startColor = startBackground as ColorDrawable val endColor = endBackground as ColorDrawable if (startColor.color == endColor.color) { return null } val animator = ValueAnimator.ofObject(ArgbEvaluator(), startColor.color, endColor.color); animator.addUpdateListener { animation -> val value = animation.getAnimatedValue() if (value != null) { view.setBackgroundColor(value as Int) } } return animator } }
Промежуточные значения генерируются автоматически, поэтому в нашем примере цвет плавно изменится с красного на синий. Этот метод открывает широкие возможности для создания самых разных кастомных анимаций и переходов: здесь фантазия разработчика ограничивается лишь требованиями конкретного проекта.
Почему это важно?
Ускорение и упрощение процесса создания анимации добавляет драйв в разработку мобильных приложений. Возможность создавать анимацию при помощи сцен, экономя время и силы, оказалась очень полезной как для разработчиков, так и для клиентов.
Расскажите о своем опыте создания анимации для Android: используете ли вы Transitions API? Какие есть плюсы и минусы у этого и других методов, с которыми вы работали?
Источник: Transitions API: делаем анимацию для Android приложений