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 приложений