April 3, 2020

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