Анимация изменений макета с помощью перехода

Попробуйте способ создания композиций.
Jetpack Compose — это рекомендуемый набор инструментов для создания пользовательского интерфейса для Android. Узнайте, как использовать анимации в Compose.

Фреймворк переходов Android позволяет анимировать любые движения в пользовательском интерфейсе, предоставляя начальный и конечный макеты. Вы можете выбрать тип анимации — например, плавное появление или исчезновение элементов, или изменение размера элементов — и фреймворк переходов определит, как анимировать переход от начального макета к конечному.

Структура перехода включает в себя следующие элементы:

  • Анимация на уровне группы: применение анимационных эффектов ко всем представлениям в иерархии представлений.
  • Встроенные анимации: используйте предопределенные анимации для распространенных эффектов, таких как затухание или движение.
  • Поддержка файлов ресурсов: загрузка иерархий представлений и встроенных анимаций из файлов ресурсов макета.
  • Обратные вызовы жизненного цикла: получают обратные вызовы, обеспечивающие управление процессом анимации и изменения иерархии.

Пример кода, демонстрирующий анимацию при изменении макета, см. в разделе BasicTransition .

Основной процесс анимации при переходе между двумя макетами выглядит следующим образом:

  1. Создайте объект Scene для начального и конечного макетов. Однако сцена начального макета часто определяется автоматически на основе текущего макета.
  2. Создайте объект Transition , чтобы определить тип анимации.
  3. Вызовите TransitionManager.go() , и система запустит анимацию для смены макетов.

Диаграмма на рисунке 1 иллюстрирует взаимосвязь между вашими макетами, сценами, переходами и финальной анимацией.

Рисунок 1. Базовая иллюстрация того, как структура перехода создает анимацию.

Создать сцену

Сцены хранят состояние иерархии представлений, включая все её представления и значения их свойств. Фреймворк переходов позволяет запускать анимацию между начальной и конечной сценами.

Вы можете создавать сцены из файла ресурсов макета или из группы представлений в вашем коде. Однако начальная сцена для перехода часто определяется автоматически на основе текущего пользовательского интерфейса.

Сцена также может определять собственные действия, которые выполняются при смене сцены. Эта функция полезна для очистки настроек отображения после перехода к новой сцене.

Создайте сцену на основе ресурса макета.

Экземпляр Scene можно создать непосредственно из файла ресурсов макета. Используйте этот метод, когда иерархия представлений в файле в основном статична. Полученная сцена представляет состояние иерархии представлений на момент создания экземпляра Scene . Если вы измените иерархию представлений, создайте сцену заново. Фреймворк создает сцену из всей иерархии представлений в файле. Вы не можете создать сцену из части файла макета.

Чтобы создать экземпляр Scene из файла ресурсов макета, получите корневой каталог сцены из вашего макета в виде ViewGroup . Затем вызовите функцию Scene.getSceneForLayout() , указав корневой каталог сцены и идентификатор ресурса файла макета, содержащего иерархию представлений для сцены.

Определите компоновку сцен.

Приведенные в остальной части этого раздела фрагменты кода показывают, как создать две разные сцены с одним и тем же корневым элементом сцены. Фрагменты также демонстрируют, что можно загрузить несколько несвязанных объектов Scene , не подразумевая при этом их взаимосвязи.

В примере используются следующие определения макета:

  • Основной макет элемента управления, содержащий текстовую метку и дочерний FrameLayout .
  • Объект ConstraintLayout для первой сцены с двумя текстовыми полями.
  • Для второй сцены ConstraintLayout с теми же двумя текстовыми полями, но в другом порядке.

В данном примере вся анимация происходит в дочернем элементе основного макета активности. Текстовая метка в основном макете остается статичной.

Основная структура данного задания определяется следующим образом:

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

Это определение макета содержит текстовое поле и дочерний FrameLayout для корневого элемента сцены. Макет для первой сцены включен в основной файл макета. Это позволяет приложению отображать его как часть начального пользовательского интерфейса, а также загружать его в сцену, поскольку фреймворк может загрузить в сцену только весь файл макета целиком.

Структура первой сцены определяется следующим образом:

res/layout/a_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view1"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
    <TextView
        android:id="@+id/text_view2"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 2"
        app:layout_constraintTop_toBottomOf="@id/text_view1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Макет для второй сцены содержит те же два текстовых поля — с теми же идентификаторами — расположенные в другом порядке. Он определяется следующим образом:

res/layout/another_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view2"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 2"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    <TextView
        android:id="@+id/text_view1"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 1"
        app:layout_constraintTop_toBottomOf="@id/text_view2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Создание сцен на основе макетов

После создания определений для двух макетов ограничений вы можете получить сцену для каждого из них. Это позволит вам переключаться между двумя конфигурациями пользовательского интерфейса. Для получения сцены вам потребуется ссылка на корневой каталог сцены и идентификатор ресурса макета.

Приведённый ниже фрагмент кода показывает, как получить ссылку на корневой каталог сцены и создать два объекта Scene из файлов макета:

Котлин

val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

Java

Scene aScene;
Scene anotherScene;

// Create the scene root for the scenes in this app.
sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes.
aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
anotherScene =
    Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

В приложении теперь есть два объекта Scene , основанных на иерархии представлений. Обе сцены используют корневой элемент сцены, определенный элементом FrameLayout в res/layout/activity_main.xml .

Создайте сцену в своем коде.

Вы также можете создать экземпляр Scene в своем коде из объекта ViewGroup . Используйте этот метод, когда вы изменяете иерархию представлений непосредственно в коде или когда генерируете ее динамически.

Для создания сцены из иерархии представлений в вашем коде используйте конструктор Scene(sceneRoot, viewHierarchy) . Вызов этого конструктора эквивалентен вызову функции Scene.getSceneForLayout() если вы уже создали файл макета.

Приведенный ниже фрагмент кода демонстрирует, как создать экземпляр Scene из корневого элемента сцены и иерархии представлений для сцены в вашем коде:

Котлин

val sceneRoot = someLayoutElement as ViewGroup
val viewHierarchy = someOtherLayoutElement as ViewGroup
val scene: Scene = Scene(sceneRoot, viewHierarchy)

Java

Scene mScene;

// Obtain the scene root element.
sceneRoot = (ViewGroup) someLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered.
viewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene.
mScene = new Scene(sceneRoot, mViewHierarchy);

Создание действий сцены

Данная платформа позволяет определять пользовательские действия, которые система выполняет при входе или выходе из сцены. Во многих случаях определение пользовательских действий не требуется, поскольку платформа автоматически анимирует переход между сценами.

Действия сцены полезны для обработки следующих случаев:

  • Для анимации элементов, находящихся в разных иерархиях, можно использовать действия выхода и входа в сцену.
  • Для анимации элементов, которые не могут быть автоматически анимированы с помощью фреймворка переходов, например, объектов ListView . Дополнительную информацию см. в разделе об ограничениях .

Для настройки действий на сцене определите свои действия как объекты Runnable и передайте их функциям Scene.setExitAction() или Scene.setEnterAction() . Фреймворк вызывает функцию setExitAction() на начальной сцене перед запуском анимации перехода и функцию setEnterAction() на конечной сцене после запуска анимации перехода.

Примените переход

Структура переходов представляет стиль анимации между сценами с помощью объекта Transition . Вы можете создать экземпляр Transition , используя встроенные подклассы, такие как AutoTransition и Fade , или определить свой собственный переход . Затем вы можете запустить анимацию между сценами, передав конечную Scene и объект Transition в метод TransitionManager.go() .

Жизненный цикл перехода аналогичен жизненному циклу активности и представляет собой состояния перехода, которые отслеживает фреймворк между началом и завершением анимации. На важных этапах жизненного цикла фреймворк вызывает функции обратного вызова, которые вы можете реализовать для корректировки пользовательского интерфейса на разных этапах перехода.

Создать переход

В предыдущем разделе показано, как создавать сцены, представляющие состояние различных иерархий представлений. После определения начальной и конечной сцен, между которыми вы хотите переключаться, создайте объект Transition , определяющий анимацию. Фреймворк позволяет либо указать встроенный переход в файле ресурсов и запустить его в коде, либо создать экземпляр встроенного перехода непосредственно в коде.

Таблица 1. Встроенные типы переходов.

Сорт Ярлык Эффект
AutoTransition <autoTransition/> Анимация по умолчанию. Затемнение, перемещение и изменение размера, а также появление элементов — именно в таком порядке.
ChangeBounds <changeBounds/> Перемещает и изменяет размер отображаемых объектов.
ChangeClipBounds <changeClipBounds/> Захватывает значение View.getClipBounds() до и после смены сцены и анимирует эти изменения во время перехода.
ChangeImageTransform <changeImageTransform/> Захватывает матрицу ImageView до и после смены сцены и анимирует её во время перехода.
ChangeScroll <changeScroll/> Отслеживает свойства прокрутки целевых объектов до и после смены сцены и анимирует любые изменения.
ChangeTransform <changeTransform/> Функция отслеживает масштаб и вращение изображений до и после смены сцены и анимирует эти изменения во время перехода.
Explode <explode/> Отслеживает изменения видимости целевых объектов в начальной и конечной сценах, а также перемещает объекты внутрь или наружу от краев сцены.
Fade <fade/> fade_in плавно отображает элементы интерфейса.
fade_out плавно затухает отображение элементов.
fade_in_out (по умолчанию) выполняет сначала fade_out а затем fade_in .
Slide <slide/> Отслеживает изменения видимости целевых объектов в начальной и конечной сценах, а также перемещает объекты внутрь или наружу от одного из краев сцены.

Создайте экземпляр перехода из файла ресурсов.

Этот метод позволяет изменять определение перехода без изменения кода вашей активности. Он также полезен для отделения сложных определений переходов от кода вашего приложения, как показано в разделе о задании нескольких переходов .

Чтобы указать встроенный переход в файле ресурсов, выполните следующие действия:

  • Добавьте каталог res/transition/ в свой проект.
  • Создайте новый XML-файл ресурсов в этой директории.
  • Добавьте XML-узел для одного из встроенных переходов.

Например, следующий файл ресурсов определяет переход Fade :

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

Следующий фрагмент кода показывает, как создать экземпляр Transition внутри вашей активности из файла ресурсов:

Котлин

var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)

Java

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

Создайте экземпляр перехода в своем коде.

Этот метод полезен для динамического создания объектов переходов при изменении пользовательского интерфейса в коде, а также для создания простых встроенных экземпляров переходов с небольшим количеством параметров или без них.

Чтобы создать экземпляр встроенного перехода, вызовите один из открытых конструкторов в подклассах класса Transition . Например, следующий фрагмент кода создает экземпляр перехода Fade :

Котлин

var fadeTransition: Transition = Fade()

Java

Transition fadeTransition = new Fade();

Примените переход

Обычно переходы используются для смены иерархии представлений в ответ на событие, например, действие пользователя. Рассмотрим, к примеру, приложение для поиска: когда пользователь вводит поисковый запрос и нажимает кнопку поиска, приложение переключается на сцену, отображающую результаты поиска, при этом применяется переход, который плавно скрывает кнопку поиска и плавно отображает результаты поиска.

Чтобы изменить сцену во время применения перехода в ответ на событие в вашей активности, вызовите функцию класса TransitionManager.go() с указанием конечной сцены и экземпляра перехода, который будет использоваться для анимации, как показано в следующем фрагменте кода:

Котлин

TransitionManager.go(endingScene, fadeTransition)

Java

TransitionManager.go(endingScene, fadeTransition);

В процессе выполнения анимации, заданной экземпляром перехода, фреймворк изменяет иерархию представлений внутри корневого элемента сцены, используя иерархию представлений из конечной сцены. Начальная сцена — это конечная сцена из последнего перехода. Если предыдущего перехода нет, начальная сцена определяется автоматически на основе текущего состояния пользовательского интерфейса.

Если вы не укажете экземпляр перехода, менеджер переходов сможет применить автоматический переход, который в большинстве случаев выполнит разумное действие. Для получения дополнительной информации см. справочник API класса TransitionManager .

Выберите конкретные целевые представления

По умолчанию фреймворк применяет переходы ко всем представлениям в начальной и конечной сценах. В некоторых случаях может потребоваться применить анимацию только к подмножеству представлений в сцене. Фреймворк позволяет выбрать конкретные представления, которые вы хотите анимировать. Например, фреймворк не поддерживает анимацию изменений объектов ListView , поэтому не пытайтесь анимировать их во время перехода.

Каждый элемент, который анимируется при переходе, называется целевым элементом . Вы можете выбирать только те целевые элементы, которые входят в иерархию элементов, связанных со сценой.

Чтобы удалить одно или несколько представлений из списка целей, вызовите метод removeTarget() перед началом перехода. Чтобы добавить в список целей только указанные представления, вызовите функцию addTarget() . Для получения дополнительной информации см. справочник API класса Transition .

Укажите несколько переходов

Чтобы анимация произвела максимальное впечатление, подберите её в соответствии с типом изменений, происходящих между сценами. Например, если вы удаляете одни элементы и добавляете другие между сценами, анимация затухания или появления обеспечит заметное указание на то, что некоторые элементы больше недоступны. Если вы перемещаете элементы в разные точки экрана, лучше анимировать движение так, чтобы пользователи заметили новое местоположение элементов.

Вам не обязательно выбирать только одну анимацию, поскольку фреймворк переходов позволяет комбинировать эффекты анимации в набор переходов, содержащий группу отдельных встроенных или пользовательских переходов.

Чтобы определить набор переходов из коллекции переходов в XML, создайте файл ресурсов в каталоге res/transitions/ и перечислите переходы в элементе TransitionSet . Например, следующий фрагмент кода показывает, как указать набор переходов, имеющий такое же поведение, как класс AutoTransition :

<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>

Чтобы преобразовать набор переходов в объект TransitionSet в вашем коде, вызовите функцию TransitionInflater.from() в вашей активности. Класс TransitionSet наследуется от класса Transition , поэтому вы можете использовать его с менеджером переходов так же, как и любой другой экземпляр Transition .

Примените переход без сцен.

Изменение иерархии представлений — не единственный способ модификации пользовательского интерфейса. Вы также можете вносить изменения, добавляя, изменяя и удаляя дочерние представления в текущей иерархии.

Например, вы можете реализовать поиск с помощью одного макета. Начните с макета, отображающего поле ввода поискового запроса и значок поиска. Чтобы изменить пользовательский интерфейс и отобразить результаты, удалите кнопку поиска при нажатии пользователем, вызвав функцию ViewGroup.removeView() , и добавьте результаты поиска, вызвав функцию ViewGroup.addView() .

Этот подход можно использовать, если альтернативой является наличие двух практически идентичных иерархий. Вместо создания и поддержки двух отдельных файлов макета для незначительного различия в пользовательском интерфейсе, можно использовать один файл макета, содержащий иерархию представлений, которую можно изменять в коде.

Если вы вносите изменения в текущую иерархию представлений таким образом, вам не нужно создавать сцену. Вместо этого вы можете создать и применить переход между двумя состояниями иерархии представлений, используя отложенный переход . Эта функция фреймворка переходов начинается с текущего состояния иерархии представлений, записывает изменения, которые вы вносите в ее представления, и применяет переход, который анимирует эти изменения при перерисовке пользовательского интерфейса системой.

Для создания отложенного перехода в рамках иерархии отдельных представлений выполните следующие шаги:

  1. Когда происходит событие, запускающее переход, вызовите функцию TransitionManager.beginDelayedTransition() , указав родительское представление для всех представлений, которые вы хотите изменить, и используемый переход. Фреймворк хранит текущее состояние дочерних представлений и значения их свойств.
  2. Вносите изменения в дочерние представления в соответствии с вашим сценарием использования. Фреймворк записывает внесенные вами изменения в дочерние представления и их свойства.
  3. Когда система перерисовывает пользовательский интерфейс в соответствии с вашими изменениями, фреймворк анимирует переходы между исходным и новым состоянием.

В следующем примере показано, как анимировать добавление текстового поля в иерархию представлений с помощью отложенного перехода. Первый фрагмент кода показывает файл определения макета:

res/layout/activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    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"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

В следующем фрагменте кода показана анимация добавления текстового поля:

Главная активность

Котлин

setContentView(R.layout.activity_main)
val labelText = TextView(this).apply {
    text = "Label"
    id = R.id.text
}
val rootView: ViewGroup = findViewById(R.id.mainLayout)
val mFade: Fade = Fade(Fade.IN)
TransitionManager.beginDelayedTransition(rootView, mFade)
rootView.addView(labelText)

Java

private TextView labelText;
private Fade mFade;
private ViewGroup rootView;
...
// Load the layout.
setContentView(R.layout.activity_main);
...
// Create a new TextView and set some View properties.
labelText = new TextView(this);
labelText.setText("Label");
labelText.setId(R.id.text);

// Get the root view and create a transition.
rootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(Fade.IN);

// Start recording changes to the view hierarchy.
TransitionManager.beginDelayedTransition(rootView, mFade);

// Add the new TextView to the view hierarchy.
rootView.addView(labelText);

// When the system redraws the screen to show this update,
// the framework animates the addition as a fade in.

Определите обратные вызовы жизненного цикла перехода.

Жизненный цикл перехода аналогичен жизненному циклу активности. Он представляет собой состояния перехода, которые отслеживает фреймворк в период между вызовом функции TransitionManager.go() и завершением анимации. На важных этапах жизненного цикла фреймворк вызывает обратные вызовы, определенные интерфейсом TransitionListener .

Колбэки жизненного цикла переходов полезны, например, для копирования значения свойства представления из начальной иерархии представлений в конечную иерархию представлений во время смены сцены. Вы не можете просто скопировать значение из начального представления в представление в конечной иерархии представлений, поскольку конечная иерархия представлений не создается до завершения перехода. Вместо этого вам необходимо сохранить значение в переменной, а затем скопировать его в конечную иерархию представлений, когда фреймворк завершит переход. Чтобы получать уведомления о завершении перехода, реализуйте функцию TransitionListener.onTransitionEnd() в вашей активности.

Для получения более подробной информации см. справочник API для класса TransitionListener .

Ограничения

В этом разделе перечислены некоторые известные ограничения структуры переходов:

  • Анимации, применяемые к SurfaceView могут отображаться некорректно. Экземпляры SurfaceView обновляются из потока, не связанного с пользовательским интерфейсом, поэтому обновления могут быть не синхронизированы с анимацией других представлений.
  • Применение некоторых типов переходов к TextureView может не давать желаемого эффекта анимации.
  • Классы, наследующие AdapterView , такие как ListView , управляют своими дочерними представлениями способами, несовместимыми с фреймворком анимаций. Если вы попытаетесь анимировать представление на основе AdapterView , дисплей устройства может перестать отвечать.
  • Если вы попытаетесь изменить размер TextView с помощью анимации, текст переместится в новое место до того, как размер объекта полностью изменится. Чтобы избежать этой проблемы, не анимируйте изменение размера элементов, содержащих текст.