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

Попробуйте способ создания
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" >
    
    
</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" >
    
    
</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)

Ява

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)

Ява

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)

Ява

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

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

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

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

Котлин

var fadeTransition: Transition = Fade()

Ява

Transition fadeTransition = new Fade();

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

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

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

Котлин

TransitionManager.go(endingScene, fadeTransition)

Ява

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)

Ява

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 с помощью анимации, текст переместится в новое место до того, как размер объекта будет полностью изменен. Чтобы избежать этой проблемы, не анимируйте изменение размера представлений, содержащих текст.