Обложка: Работа с анимацией в Android: разбираем MotionLayout

Работа с анимацией в Android: разбираем MotionLayout

Эльза Хамдеева
Эльза Хамдеева

Android-разработчик ИТ-компании MediaSoft

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

Когда встаёт вопрос о реализации достаточно сложного UI, для разметки экрана разработчики чаще всего используют ConstraintLayout — достаточно популярный и гибкий инструмент, предоставляющий широкие возможности для вёрстки.

Официальная документация: ConstraintLayout — это ViewGroup, который даёт возможность гибко перемещать виджеты и изменять их размер. Он позволяет избавиться от большого числа вложенностей и улучшает производительность layout. Летом 2020 года вышел стабильный релиз ConstraintLayout 2.0, который позволяет задавать анимацию объектам с помощью MotionLayout.

Официальная документация: MotionLayout — это layout, который расширяет ConstraintLayout и позволяет анимировать layouts между различными состояниями. А для создания анимации в большинстве случаев достаточно только вёрстки в файле XML. Таким образом, программный код становится чище и лаконичнее.

Ещё одним примечательным нововведением стал MotionEditor — простой интерфейс для управления элементами из библиотеки MotionLayout. MotionEditor доступен в AndroidStudio, начиная с версии 4.0, и работу с ним мы также рассмотрим в данной статье.

Let’s do animations

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

  • наличие AndroidStudio 4.0+;
  • базовые знания ConstraintLayout;
  • немного Kotlin.

Для начала необходимо добавить зависимость в gradle:

implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

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

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/mouse"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_mouse"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

Движение объектов

Основную работу мы будем делать в файле с motion_scene. Для его создания необходимо кликнуть правой кнопкой мыши по превью объекта, которое вы хотите анимировать (наша мышь), и в выпадающем списке выбрать пункт Convert to MotionLayout.

Наш корневой ConstraintLayout преобразовался в MotionLayout, а атрибут LayoutDescription указывает на сгенерированный layout_scene-файл. С ним мы и будем работать.

В этом файле лежит всё, что необходимо для создания анимации:

  • корневой элемент MotionScene;
  • ConstraintSets — настройки для описания состояния экрана;
  • Transition — переход между состояниями;
  • KeyFrames и другие атрибуты.

Мощным инструментом для конфигурации MotionLayot является уже упомянутый MotionEditor. На экране редактора можно увидеть состояния start и end. Между ними стрелка, которая показывает переход между состояниями или Transiton.

Редактировать Transitions и ConstraintSets можно с помощью панели справа, где задаются различные атрибуты (свойства): constraintSetStart — это начальная позиция нашей мышки (в нашем случае верх экрана). Конечную позицию объекта (constraintSetEnd) определим внизу экрана — для этого добавим необходимые привязки:

<ConstraintSet android:id="@+id/end">
<Constraint android:id="@+id/mouse">
<Layout
android:layout_width="50dp"
android:layout_height="50dp"
motion:layout_constraintBottom_toBottomOf="parent" />
</Constraint>
</ConstraintSet>

Запустим Transition и посмотрим, что будет происходить на экране — наша мышка бежит сверху вниз.

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

  1. Указываем в качестве начального ConstraintSet — end, в качестве конечного — start.
  2. В поле Automatically (атрибут autoTransition в файле XML) выбираем Animate to End, чтобы анимация проигрывалась от начала до конца.
  3. В коде добавим значение duration (длительность анимации в миллисекундах) — 5000, чтобы мышь двигалась чуть медленнее.

Обратите внимание, что атрибут autoTransition позволяет запускать переходы автоматически друг за другом. Таким образом, состояние объекта будет меняться от start до end и от end до start автоматически.

 

<Transition
   motion:autoTransition="animateToEnd"
   motion:constraintSetEnd="@+id/end"
   motion:constraintSetStart="@id/start"
   motion:duration="5000">
   <KeyFrameSet />
</Transition>

<Transition
   motion:autoTransition="animateToEnd"
   motion:constraintSetEnd="@+id/start"
   motion:constraintSetStart="@+id/end"
   motion:duration="5000" />

 

Теперь предлагаю сделать траекторию движения объекта разнообразнее. Какие есть варианты? Можно задать больше промежуточных ConstraintSet с разным положением мышки на экране, но данный подход будет не самым удобным, так как нам необходимо определять большое количество промежуточных состояний между основными. Создатели MotionLayout предлагают использовать более легковесные объекты KeyFrames.

KeyFrames определяют позицию или свойства объекта, а также изменения в конкретной временной точке перехода (анимации).
Для того чтобы открыть окно создания KeyFrame из редактора, необходимо выбрать нужный нам Transition, на временной шкале указать точку (именно в этот момент перехода применяется KeyFrame) и нажать на кнопку создания в правом верхнем углу.

Сконфигурируем KeyFrame в открывшемся окне:

  1. Выбираем из списка KeyPosition, поскольку мы хотим задать позицию объекта на экране. Этот атрибут указывает, в какой момент выполнения Transition применяется KeyFrame (синяя стрелка на скриншоте). Значение может варьироваться от 0 до 100.
  2. Выбираем систему координат pathRelative, чтобы движение объекта отклонялось от основного пути в разные стороны.
  3. Заполняем атрибуты percentX и percentY — положение объекта в виде пары координат (x, y). Обратите внимание, что значения могут быть отрицательными.

Значения percentX и percentY зависят от системы координат, которую мы укажем в атрибуте KeyPosition:

  • parentRelative — координаты указывают на положение относительно родительского контейнера, в котором расположена View;
  • deltaRelative — координаты указывают на положение относительно стартовой и конечной позиции в процентном соотношении;
  • pathRelative — координаты указывают на положение относительно пути от стартовой до конечной позиции объекта.

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

Итак, первый KeyPosition есть! Зададим ещё несколько, чтобы мышка плавно перемещалась из стороны в сторону. Кстати, в MotionEditor есть возможность вручную перетащить объект на нужную позицию при определении KeyFrames.

После определения произвольных KeyFrames на обеих Transitions получаем анимацию «убегающей» мышки.

Изменения свойств объекта

Теперь усложним нашу анимацию, изменив свойства объекта. Для этого добавим в файл вёрстки ещё один ImageView, пусть это будет мяч. Котики ведь бегают за мячами?

Сделаем так, чтобы мяч летел из нижнего левого угла в правый верхний. Для этого добавим новый Constraint в уже существующие ConstraintSets и с помощью тега Layout укажем необходимые привязки:

<Transition
motion:autoTransition="animateToEnd"
motion:constraintSetEnd="@+id/start"
motion:constraintSetStart="@+id/end"
motion:duration="5000" />
<ConstraintSet android:id="@+id/start">
   <Constraint android:id="@id/mouse">
       <Layout
           android:layout_width="50dp"
           android:layout_height="50dp"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />
   </Constraint>
   <Constraint android:id="@+id/ball">
       <Layout
           android:layout_width="50dp"
           android:layout_height="50dp"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintStart_toStartOf="parent" />
   </Constraint>
</ConstraintSet>

Добавим к нашим переходам KeyAttributes, которые будут менять свойства мячика во время переходов по тому же принципу, что и KeyPositions. В окне конфигурации KeyAttributes выбираем id нашего мяча и свойство. Вы можете изменить размер, добавить поворот, прозрачность, тень и другие свойства объекта. Например, для того, чтобы повернуть мяч на 180 градусов, в меню Attributes укажем значение rotation 180.

Таким же образом поменяем размеры мячика (scaleX, scaleY) и прозрачность (alpha) в разных временных точках наших переходов. Произвольным образом добавляем новые KeyFrames.

<KeyAttribute
   motion:motionTarget="@+id/ball"
   motion:framePosition="10"
   android:alpha="0.5" />
<KeyAttribute
   motion:motionTarget="@+id/ball"
   motion:framePosition="25"
   android:alpha="0" />
<KeyAttribute
   motion:motionTarget="@+id/ball"
   motion:framePosition="45"
   android:alpha="0.5" />
<KeyAttribute
   motion:motionTarget="@+id/ball"
   motion:framePosition="45"
   android:scaleX="0.5"
   android:scaleY="0.5" />
<KeyAttribute
   motion:motionTarget="@+id/ball"
   motion:framePosition="75"
   android:scaleX="1.5"
   android:scaleY="1.5" />

Добавим счётчик нажатий на объекты, для этого повесим ClickListeners на мышку и мячик, внутри которых будем увеличивать значение счётчика на единицу и выводить его на экране. Добавим фон и наше мини-приложение для котиков готово!

Итак, мы с вами рассмотрели базовые понятия MotionLayot и написали мини-игру для котика, используя только вёрстку!

Вы можете продолжить своё обучение, например, рассмотреть CustomAttribute, OnSwipe, OnClick, использование MotionLayout вместе с CoordinatorLayout, DrawerLayout, ViewPager, и другое. Всё это позволит вам создавать разнообразные анимированные эффекты: например, скрываемый тулбар, изменение цвета фона, анимацию по клику и свайпу, анимацию при перелистывании страниц, открытие и закрытие меню и многое другое. При этом вы будете поддерживать простую UI-логику и значительным образом экономить код.