Продвинутый Android в Kotlin 03.2: Анимация с MotionLayout

Эта лаборатория кода является частью курса Advanced Android in Kotlin. Вы получите максимальную пользу от этого курса, если будете последовательно работать с кодовыми таблицами, но это не обязательно. Все кодовые таблицы курсов перечислены на целевой странице Advanced Android in Kotlin codelabs .

MotionLayout - это библиотека, которая позволяет добавлять движения в ваше приложение для Android. Он основан на ConstraintLayout, и позволяет вам анимировать все, что вы можете построить с помощью ConstraintLayout .

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

Анимация - отличный способ улучшить работу с приложением. Вы можете использовать анимацию, чтобы:

  • Показать изменения - анимация между состояниями позволяет пользователю естественным образом отслеживать изменения в вашем пользовательском интерфейсе.
  • Привлекайте внимание - используйте анимацию, чтобы привлечь внимание к важным элементам пользовательского интерфейса.
  • Создавайте красивый дизайн - эффективное движение в дизайне делает приложения безупречными.

Предпосылки

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

  • Знайте, как создать приложение с действием, базовым макетом и запустить его на устройстве или эмуляторе с помощью Android Studio. Ознакомьтесь с ConstraintLayout . Прочтите кодовую лабораторию Constraint Layout, чтобы узнать больше о ConstraintLayout .

Что ты будешь делать

  • Определите анимацию с помощью ConstraintSets и MotionLayout
  • Анимация на основе событий перетаскивания
  • Измените анимацию с помощью KeyPosition
  • Изменить атрибуты с помощью KeyAttribute
  • Запускать анимацию с помощью кода
  • Анимируйте сворачиваемые заголовки с помощью MotionLayout

Что вам понадобится

  • Android Studio 4.0 (редактор MotionLayout работает только с этой версией Android Studio.)

Чтобы загрузить образец приложения, вы можете:

Скачать Zip

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

$ git clone https://github.com/googlecodelabs/motionlayout.git

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

Чтобы создать анимацию из начального кода, вам понадобятся следующие основные части:

  • MotionLayout, который является подклассом ConstraintLayout . Вы указываете все виды для анимации внутри тега MotionLayout .
  • MotionScene, представляющий собой XML-файл, описывающий анимацию для MotionLayout.
  • Transition, который является частью MotionScene , задает продолжительность анимации, триггер и способ перемещения представлений.
  • ConstraintSet который определяет как начальные, так и конечные ограничения перехода.

Давайте рассмотрим каждый из них по очереди, начиная с MotionLayout .

Шаг 1. Изучите существующий код

MotionLayout является подклассом ConstraintLayout , поэтому он поддерживает все те же функции при добавлении анимации. Чтобы использовать MotionLayout , вы добавляете представление MotionLayout котором вы бы использовали ConstraintLayout.

  1. В res/layout откройте activity_step1.xml. Здесь у вас есть ConstraintLayout с одним ImageView звезды, внутри которого применен оттенок.

activity_step1.xml

<!-- initial code -->
<androidx.constraintlayout.widget.ConstraintLayout
       ...
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       >

   <ImageView
           android:id="@+id/red_star"
           ...
   />

</androidx.constraintlayout.motion.widget.MotionLayout>

Этот ConstraintLayout не имеет никаких ограничений, поэтому, если вы запустите приложение сейчас, вы увидите, что звездочка отображается без ограничений, что означает, что они будут расположены в неизвестном месте. Android Studio выдаст предупреждение об отсутствии ограничений.

Шаг 2. Преобразование в макет движения

Для анимации с использованием MotionLayout, необходимо преобразовать ConstraintLayout в MotionLayout .

Чтобы ваш макет использовал сцену движения, он должен указывать на нее.

  1. Для этого откройте область конструктора. В Android Studio 4.0 вы открываете область дизайна, используя значок разделения или дизайна в правом верхнем углу при просмотре XML-файла макета.

a2beea710c2decb7.png

  1. После открытия области конструктора щелкните правой кнопкой мыши предварительный просмотр и выберите « Преобразовать в MotionLayout» .

4fa936a98a8393b9.png

Это заменяет тег ConstraintLayout тег MotionLayout и добавляет motion:layoutDescription к тегу MotionLayout который указывает на @xml/activity_step1_scene.

activity_step1 **. xml **

<!-- explore motion:layoutDescription="@xml/activity_step1_scene" -->
<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:layoutDescription="@xml/activity_step1_scene">

Сцена движения - это отдельный файл XML, который описывает анимацию в MotionLayout .

Как только вы конвертируете в MotionLayout , в MotionLayout области появится редактор движения.

66d0e80d5ab4daf8.png

В редакторе движения появилось три новых элемента пользовательского интерфейса:

  1. Обзор - это модальный выбор, который позволяет вам выбирать различные части анимации. На этом изображении выбран start ConstraintSet . Вы также можете выбрать переход между start и end , щелкнув стрелку между ними.
  2. Раздел - под обзором находится окно раздела, которое изменяется в зависимости от текущего выбранного элемента обзора. На этом изображении start информация ConstraintSet отображается в окне выбора.
  3. Атрибут - панель атрибутов показывает и позволяет редактировать атрибуты текущего выбранного элемента либо из окна обзора, либо из окна выбора. На этом изображении показаны атрибуты для start ConstraintSet .

Шаг 3. Определите начальные и конечные ограничения

Все анимации можно определить как начало и конец. Начало описывает, как выглядел экран до анимации, а конец описывает, как выглядит экран после завершения анимации. MotionLayout отвечает за определение того, как анимировать между начальным и конечным состояниями (с течением времени).

MotionScene использует тег ConstraintSet для определения начального и конечного состояний. ConstraintSet - это то, на что это похоже, набор ограничений, которые могут применяться к представлениям. Сюда входят ограничения ширины, высоты и ConstraintLayout . Он также включает некоторые атрибуты, такие как alpha . Он не содержит самих представлений, только ограничения на эти представления.

Любые ограничения, указанные в ConstraintSet , переопределят ограничения, указанные в файле макета. Если вы определяете ограничения как в макете, так и в MotionScene , применяются только ограничения в MotionScene .

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

Вы можете выполнить этот шаг либо с помощью редактора движения, либо напрямую отредактировав текст activity_step1_scene.xml .

  1. Выберите start ConstraintSet на панели обзора.

6e57661ed358b860.png

  1. На панели выбора выберите red_star . В настоящее время он показывает Источник layout - это означает, что он не ограничен этим ConstraintSet . Используйте значок карандаша в правом верхнем углу, чтобы создать ограничение.

f9564c574b86ea8.gif

  1. Убедитесь, что red_star показывает источник start когда на панели обзора выбран параметр start ConstraintSet .
  2. На панели «Атрибуты», red_star в start ConstraintSet , добавьте ограничение вверху и начните, нажимая синие кнопки « .

2fce076cd7b04bd.png

  1. Откройте xml/activity_step1_scene.xml чтобы увидеть код, созданный редактором движения для этого ограничения.

activity_step1_scene.xml

<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

ConstraintSet имеет id @id/start и определяет все ограничения, применяемые ко всем представлениям в MotionLayout . Поскольку этот MotionLayout имеет только одно представление, ему требуется только одно Constraint .

Constraint внутри ConstraintSet определяет идентификатор представления, которое оно ограничивает, @id/red_star определенный в activity_step1.xml . Важно отметить, что теги Constraint определяют только ограничения и информацию о макете. Тег Constraint не знает, что он применяется к ImageView .

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

  1. Выберите end ConstraintSet на обзорной панели.

346e1248639b6f1e.png

  1. Выполните те же шаги, что и раньше, чтобы добавить Constraint для red_star в end ConstraintSet .
  2. Чтобы использовать редактор движения для выполнения этого шага, добавьте ограничение в end и bottom , нажав синие кнопки + .

fd33c779ff83c80a.png

  1. Код в XML выглядит так:

activitiy_step1_scene.xml

<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

Как и @id/start , этот ConstraintSet имеет единственное Constraint на @id/red_star . На этот раз он ограничивает его нижним краем экрана.

Необязательно называть их @id/start и @id/end , но это удобно.

Шаг 4. Определите переход

Каждая MotionScene также должна включать как минимум один переход. Переход определяет каждую часть одной анимации от начала до конца.

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

  1. Редактор движения создал для нас переход по умолчанию при создании файла MotionScene. Откройте файл activity_step1_scene.xml чтобы увидеть сгенерированный переход.

activity_step1_scene.xml

<!-- A transition describes an animation via start and end state -->
<Transition
   motion:constraintSetEnd="@+id/end"
   motion:constraintSetStart="@id/start"
   motion:duration="1000">
  <KeyFrameSet>
  </KeyFrameSet>
</Transition>

Это все, что нужно MotionLayout для создания анимации. Глядя на каждый атрибут:

  • constraintSetStart будет применяться к представлениям при запуске анимации.
  • constraintSetEnd будет применен к представлениям в конце анимации.
  • duration указывает продолжительность анимации в миллисекундах.

MotionLayout определит путь между начальным и конечным ограничениями и анимирует его на указанную продолжительность.

Шаг 5. Предварительный просмотр анимации в редакторе движения

dff9ecdc1f4a0740.gif

Анимация: видео воспроизведения предварительного просмотра перехода в редакторе движения.

  1. Откройте редактор движения и выберите переход, щелкнув стрелку между start и end на панели обзора.

1dc541ae8c43b250.png

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

a0fd2593384dfb36.png

Шаг 6. Добавьте обработчик кликов

Вам нужен способ запустить анимацию. Один из способов сделать это - заставить MotionLayout реагировать на события щелчка на @id/red_star .

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

b6f94b344ce65290.png

  1. Нажмите 699f7ae04024ccf6.png Создайте обработчик щелчка или смахивания на панели инструментов для панели обзора. Это добавляет обработчик, который запустит переход.
  2. Выберите Click Handler во всплывающем окне.

ccf92d06335105fe.png

  1. Измените View To Click на red_star .

b0d3f0c970604f01.png

  1. Щелкните Добавить обработчик щелчка, представленный маленькой точкой в ​​редакторе перехода в движении.

cec3913e67fb4105.png

  1. С переходом выбранным в обзорной панели, добавьте clickAction атрибут toggle к добавленному в панели атрибутов обработчика OnClick.

9af6fc60673d093d.png

  1. Откройте файл activity_step1_scene.xml чтобы увидеть код, созданный редактором движения.

activity_step1_scene.xml

<!-- A transition describes an animation via start and end state -->
<Transition
    motion:constraintSetStart="@+id/start"
    motion:constraintSetEnd="@+id/end"
    motion:duration="1000">
    <!-- MotionLayout will handle clicks on @id/red_star to "toggle" the animation between the start and end -->
    <OnClick
        motion:targetId="@id/red_star"
        motion:clickAction="toggle" />
</Transition>

Transition сообщает MotionLayout запускать анимацию в ответ на события щелчка с помощью <OnClick> . Глядя на каждый атрибут:

  • targetId - это представление, на которое нужно следить за кликами.
  • clickAction toggle будет переключаться между начальным и конечным состоянием при щелчке. Вы можете увидеть другие параметры для clickAction в документации .
  1. Запустите свой код, нажмите Шаг 1 , затем нажмите красную звездочку и посмотрите анимацию!

Шаг 5. Анимация в действии

Запустите приложение! Вы должны увидеть, как ваша анимация запускается, когда вы нажимаете на звездочку.

7ba88af963fdfe10.gif

Завершенный файл сцены движения определяет один Transition который указывает на начало и конец ConstraintSet .

В начале анимации ( @id/start ) значок звездочки ограничивается верхним началом экрана. В конце анимации ( @id/end ) значок звездочки ограничивается нижним краем экрана.

<?xml version="1.0" encoding="utf-8"?>

<!-- Describe the animation for activity_step1.xml -->
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:android="http://schemas.android.com/apk/res/android">
   <!-- A transition describes an animation via start and end state -->
   <Transition
           motion:constraintSetStart="@+id/start"
           motion:constraintSetEnd="@+id/end"
           motion:duration="1000">
       <!-- MotionLayout will handle clicks on @id/star to "toggle" the animation between the start and end -->
       <OnClick
               motion:targetId="@id/red_star"
               motion:clickAction="toggle" />
   </Transition>

   <!-- Constraints to apply at the end of the animation -->
   <ConstraintSet android:id="@+id/start">
       <Constraint
               android:id="@+id/red_star"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               motion:layout_constraintStart_toStartOf="parent"
               motion:layout_constraintTop_toTopOf="parent" />
   </ConstraintSet>

   <!-- Constraints to apply at the end of the animation -->
   <ConstraintSet android:id="@+id/end">
       <Constraint
               android:id="@+id/red_star"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintBottom_toBottomOf="parent" />
   </ConstraintSet>
</MotionScene>

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

Шаг 1. Проверьте исходный код

  1. Для начала откройте файл макета activity_step2.xml , в котором уже есть MotionLayout . Взгляните на код.

activity_step2.xml

<!-- initial code -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:layoutDescription="@xml/step2" >

   <ImageView
           android:id="@+id/left_star"
           ...
   />

   <ImageView
           android:id="@+id/right_star"
           ...
   />

   <ImageView
           android:id="@+id/red_star"
           ...
   />

   <TextView
           android:id="@+id/credits"
           ...
           motion:layout_constraintTop_toTopOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

Этот макет определяет все представления для анимации. Значки с тремя звездочками не ограничены в макете, потому что они будут анимированы в сцене движения.

К титрам TextView применены ограничения, потому что он остается в одном месте для всей анимации и не изменяет никаких атрибутов.

Шаг 2: Анимируйте сцену

Как и в последней анимации, анимация будет определяться начальным и конечным ConstraintSet, и Transition .

Определите начальный ConstraintSet

  1. Откройте сцену движения xml/step2.xml чтобы определить анимацию.
  2. Добавьте ограничения для начального ограничения start . Вначале все три звезды расположены по центру внизу экрана. Правая и левая звездочки имеют alpha значение 0.0 , что означает, что они полностью прозрачны и скрыты.

step2.xml

<!-- TODO apply starting constraints -->

<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />

   <Constraint
           android:id="@+id/left_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="0.0"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />

   <Constraint
           android:id="@+id/right_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="0.0"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

В этом ConstraintSet вы указываете одно Constraint для каждой звезды. Каждое ограничение будет применяться MotionLayout в начале анимации.

Каждый вид звезды центрируется в нижней части экрана с использованием ограничений начала, конца и низа. Две звезды @id/left_star и @id/right_star имеют дополнительное альфа-значение, которое делает их невидимыми и применяется в начале анимации.

Наборы ограничений start и end определяют начало и конец анимации. Ограничение на начало, например motion:layout_constraintStart_toStartOf , ограничит начало представления началом другого представления. Сначала это может сбивать с толку, потому что start имени используется для обоих, и они оба используются в контексте ограничений. Чтобы помочь выявить различие, start в layout_constraintStart относится к «началу» представления, которое находится слева на языке слева направо и справа на языке справа налево. Набор start ограничений относится к началу анимации.

Определите конец ConstraintSet

  1. Определите конечное ограничение, чтобы использовать цепочку для размещения всех трех звезд вместе под @id/credits . Кроме того, он установит конечное значение alpha левой и правой звезд на 1.0 .

step2.xml

<!-- TODO apply ending constraints -->

<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">

   <Constraint
           android:id="@+id/left_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="1.0"
           motion:layout_constraintHorizontal_chainStyle="packed"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toStartOf="@id/red_star"
           motion:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toEndOf="@id/left_star"
           motion:layout_constraintEnd_toStartOf="@id/right_star"
           motion:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/right_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="1.0"
           motion:layout_constraintStart_toEndOf="@id/red_star"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/credits" />
</ConstraintSet>

Конечным результатом является то, что виды будут расширяться и подниматься вверх от центра по мере их анимации.

Кроме того, поскольку свойство alpha установлено на @id/right_start и @id/left_star в обоих ConstraintSets , оба представления будут исчезать по мере продвижения анимации.

Анимация на основе свайпа пользователя

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

  1. Замените TODO для добавления тега OnSwipe на <OnSwipe motion:touchAnchorId="@id/red_star" /> .

step2.xml

<!-- TODO add OnSwipe tag -->

<!-- A transition describes an animation via start and end state -->
<Transition
       motion:constraintSetStart="@+id/start"
       motion:constraintSetEnd="@+id/end">
   <!-- MotionLayout will track swipes relative to this view -->
   <OnSwipe motion:touchAnchorId="@id/red_star" />
</Transition>

OnSwipe содержит несколько атрибутов, наиболее важным из которых является touchAnchorId .

  • touchAnchorId - это отслеживаемое представление, которое перемещается в ответ на прикосновение. MotionLayout сохранит это представление на том же расстоянии от пальца, который проводит пальцем.
  • touchAnchorSide определяет, какую сторону представления следует отслеживать. Это важно для видов, которые изменяют размер, следуют сложным путям или имеют одну сторону, которая движется быстрее, чем другая.
  • dragDirection определяет, какое направление имеет значение для этой анимации (вверх, вниз, влево или вправо).

Когда MotionLayout прослушивает события перетаскивания, слушатель будет зарегистрирован в представлении MotionLayout а не в представлении, указанном в touchAnchorId . Когда пользователь начинает жест в любом месте экрана, MotionLayout сохраняет расстояние между его пальцем и touchAnchorSide touchAnchorId представления touchAnchorId . Например, если они касаются на 100dp от стороны привязки, MotionLayout будет удерживать эту сторону на 100dp от их пальца на протяжении всей анимации.

Попробуй это

  1. Снова запустите приложение и откройте экран шага 2. Вы увидите анимацию.
  2. Попробуйте «бросить» или отпустить палец на середине анимации, чтобы изучить, как MotionLayout отображает анимацию на основе физики жидкости!

fefcdd690a0dcaec.gif

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

В этой анимации все три представления располагаются относительно своего родителя в нижней части экрана для запуска. В конце три представления позиционируются относительно @id/credits в цепочке.

Несмотря на эти очень разные макеты, MotionLayout будет создавать MotionLayout анимацию между началом и концом.

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

Шаг 1. Изучите существующий код

  1. Откройте layout/activity_step3.xml и xml/step3.xml чтобы увидеть существующий макет и сцену движения. ImageView и TextView отображают текст луны и титров.
  2. Откройте файл сцены движения ( xml/step3.xml ). Вы видите, что определен Transition от @id/start к @id/end . Анимация перемещает изображение луны из нижнего левого угла экрана в нижний правый ConstraintSets экрана с помощью двух ConstraintSets . Текст титров постепенно меняется от alpha="0.0" до alpha="1.0" мере движения луны.
  3. Запустите приложение сейчас и выберите Шаг 3 . Вы увидите, что луна следует линейной траекторией (или прямой линией) от начала до конца, когда вы нажмете на луну.

Шаг 2. Включите отладку пути

Прежде чем добавлять дугу к движению луны, полезно включить отладку пути в MotionLayout .

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

  1. Чтобы включить пути отладки, откройте layout/activity_step3.xml и добавьте motion:motionDebug="SHOW_PATH" в тег MotionLayout .

activity_step3.xml

<!-- Add motion:motionDebug="SHOW_PATH" -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:motionDebug="SHOW_PATH" >

После того, как вы включите отладку пути, когда вы снова запустите приложение, вы увидите пути всех представлений, визуализированных пунктирной линией.

23bbb604f456f65c.png

  • Круги обозначают начальную или конечную позицию одного вида.
  • Линии представляют путь одного вида.
  • Алмазы представляют собой KeyPosition , который изменяет путь.

Например, в этой анимации средний круг - это позиция текста титров.

Шаг 3. Измените путь

Все анимации в MotionLayout определяются началом и концом ConstraintSet который определяет, как будет выглядеть экран до начала и после завершения анимации. По умолчанию MotionLayout строит линейный путь (прямую линию) между начальной и конечной позицией каждого вида, который меняет положение.

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

  1. Откройте xml/step3.xml и добавьте KeyPosition в сцену. Тег KeyPosition помещается внутри тега Transition .

eae4dae9a12d0410.png

step3.xml

<!-- TODO: Add KeyFrameSet and KeyPosition -->
<KeyFrameSet>
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
   />
</KeyFrameSet>

KeyFrameSet является ребенком в Transition , и это представляет собой набор из всех KeyFrames , таких как KeyPosition , которые должны быть применены во время перехода.

Поскольку MotionLayout вычисляет путь для луны между началом и концом, он изменяет путь на основе KeyPosition указанного в KeyFrameSet . Вы можете увидеть, как это изменяет путь, снова запустив приложение.

KeyPosition имеет несколько атрибутов, описывающих, как он изменяет путь. Наиболее важные из них:

  • framePosition - это число от 0 до 100. Оно определяет, когда в анимации следует применять это KeyPosition , при этом 1 соответствует 1% через анимацию, а 99 - 99% через анимацию. Итак, если значение равно 50, вы применяете его прямо посередине.
  • motionTarget - это представление, для которого этот KeyPosition изменяет путь.
  • keyPositionType - это то, как этот KeyPosition изменяет путь. Это может быть parentRelative , pathRelative или deltaRelative (как описано на следующем шаге).
  • percentX | percentY - это percentX | percentY изменения пути в framePosition (значения от 0,0 до 1,0, с допустимыми отрицательными значениями и значениями> 1).

Вы можете думать об этом так: «В framePosition измените путь motionTarget , переместив его на percentX или percentY соответствии с координатами, определенными keyPositionType **.» **

По умолчанию MotionLayout любые углы, появившиеся при изменении пути. Если вы посмотрите на только что созданную анимацию, вы увидите, что луна движется по кривой траектории на изгибе. Для большинства анимаций это именно то, что вам нужно, а если нет, вы можете указать атрибут curveFit чтобы настроить его.

Попробуй это

Если вы снова запустите приложение, вы увидите анимацию для этого шага.

46b179c01801f19e.gif

Луна следует по дуге, потому что она проходит через KeyPosition указанное в Transition .

<KeyPosition
       motion:framePosition="50"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.5"
/>

Вы можете прочитать это KeyPosition как: «В framePosition 50 (на середине анимации) измените путь motionTarget @id/moon , переместив его на 50% Y (на полпути вниз по экрану) в соответствии с координатами, определенными parentRelative (весь MotionLayout ). "

Итак, в середине анимации луна должна пройти через KeyPosition которое на 50% ниже на экране. Это KeyPosition вообще не изменяет движение X, поэтому луна по-прежнему будет двигаться от начала до конца по горизонтали. MotionLayout определит плавный путь, который проходит через этот KeyPosition при перемещении между началом и концом.

Если вы присмотритесь, текст титров ограничен положением луны. Почему он тоже не движется по вертикали?

1c7cf779931e45cc.gif

<Constraint
       android:id="@id/credits"
       ...
       motion:layout_constraintBottom_toBottomOf="@id/moon"
       motion:layout_constraintTop_toTopOf="@id/moon"
/>

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

Если вы хотите, чтобы кредиты перемещались вместе с луной, вы можете добавить KeyPosition к кредитам или изменить начальные ограничения для @id/credits .

В следующем разделе вы погрузитесь в различные типы keyPositionType в MotionLayout .

На последнем этапе вы использовали keyPosition тип parentRelative , чтобы компенсировать путь на 50% экрана. Атрибут keyPositionType определяет, как MotionLayout будет изменять путь в соответствии с percentX или percentY .

<KeyFrameSet>
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
   />
</KeyFrameSet>

Возможны три различных типа keyPosition : parentRelative , pathRelative и deltaRelative . Указание типа изменит систему координат, по которой percentX percentY percentX и percentY

Что такое система координат?

Система координат позволяет указать точку в пространстве. Они также полезны для описания позиции на экране.

MotionLayout координат MotionLayout являются декартовой системой координат . Это означает, что у них есть оси X и Y, определяемые двумя перпендикулярными линиями. Ключевое различие между ними заключается в том, где на экране идет ось X (ось Y всегда перпендикулярна оси X).

Все системы координат в MotionLayout используют значения от 0.0 до 1.0 по осям X и Y. Они допускают отрицательные значения и значения больше 1.0 . Так, например, значение percentX равное -2.0 будет означать, что percentX дважды пройти в направлении, противоположном оси X.

Если все это звучит слишком похоже на урок алгебры, посмотрите картинки ниже!

parent Относительные координаты

a7b7568d46d9dec7.png

keyPositionType parentRelative использует ту же систему координат, что и экран. Он определяет (0, 0) в верхнем левом углу всего MotionLayout и (1, 1) в правом нижнем углу.

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

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

delta Относительные координаты

5680bf553627416c.png

Дельта - это математический термин для обозначения изменения, поэтому deltaRelative - это способ сказать «относительное изменение». В координатах deltaRelative (0,0) - это начальная позиция представления, а (1,1) - конечная позиция. Оси X и Y выровнены с экраном.

Ось X всегда горизонтальна на экране, а ось Y всегда вертикальна на экране. По сравнению с parentRelative , основное отличие состоит в том, что координаты описывают только ту часть экрана, в которой будет перемещаться представление.

deltaRelative - отличная система координат для изолированного управления горизонтальным или вертикальным движением. Например, вы можете создать анимацию, которая завершает только свое вертикальное (Y) движение на 50% и продолжает анимацию по горизонтали (X).

p ath Относительные координаты

f3aaadaac8b4a93f.png

Последняя система координат в MotionLayout - pathRelative . Он сильно отличается от двух других, поскольку ось X следует за траекторией движения от начала до конца. Итак, (0,0) - это начальная позиция, а (1,0) - конечная позиция.

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

Оказывается, pathRelative действительно полезен для нескольких вещей.

  • Ускорение, замедление или остановка просмотра во время части анимации. Поскольку размер X всегда будет точно соответствовать пути, по которому проходит представление, вы можете использовать pathRelative KeyPosition чтобы изменить pathRelative KeyPosition в котором framePosition конкретная точка на этом пути. Таким образом, KeyPosition в framePosition="50" с percentX="0.1" приведет к тому, что анимация займет 50% времени на прохождение первых 10% движения.
  • Добавление тонкой дуги к пути. Поскольку размер Y всегда перпендикулярен движению, изменение Y изменит путь к кривой относительно общего движения.
  • Добавление второго измерения, когда deltaRelative не работает. Для полностью горизонтального и вертикального движения deltaRelative создаст только одно полезное измерение. Однако pathRelative всегда будет создавать пригодные для использования координаты X и Y.

In the next step you'll learn how to build even more complex paths using more than one KeyPosition .

Looking at the animation you built in the last step, it does create a smooth curve, but the shape could be more "moon like."

Modify a path with multiple KeyPosition elements

MotionLayout can modify a path further by defining as many KeyPosition as needed to get any motion. For this animation you will build an arc, but you could make the moon jump up and down in the middle of the screen, if you wanted.

  1. Open xml/step4.xml . You see it has the same views and the KeyFrame you added in the last step.
  2. To round out the top of the curve, add two more KeyPositions to the path of @id/moon , one just before it reaches the top, and one after.

500b5ac2db48ef87.png

step4.xml

<!-- TODO: Add two more KeyPositions to the KeyFrameSet here -->
<KeyPosition
       motion:framePosition="25"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.6"
/>
<KeyPosition
       motion:framePosition="75"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.6"
/>

These KeyPositions will be applied 25% and 75% of the way through the animation, and cause @id/moon to move through a path that is 60% from the top of the screen. Combined with the existing KeyPosition at 50%, this creates a smooth arc for the moon to follow.

In MotionLayout , you can add as many KeyPositions as you would need to get the motion path you want. MotionLayout will apply each KeyPosition at the specified framePosition , and figure out how to create a smooth motion that goes through all of the KeyPositions .

Try it out

  1. Run the app again. Go to Step 4 to see the animation in action. When you click on the moon, it follows the path from start to end, going through each KeyPosition that was specified in the KeyFrameSet .

Explore on your own

Before you move on to other types of KeyFrame , try adding some more KeyPositions to the KeyFrameSet to see what kind of effects you can create just using KeyPosition .

Here's one example showing how to build a complex path that moves back and forth during the animation.

cd9faaffde3dfef.png

step4.xml

<!-- Complex paths example: Dancing moon -->
<KeyFrameSet>
   <KeyPosition
           motion:framePosition="25"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.6"
           motion:percentX="0.1"
   />
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
           motion:percentX="0.3"
   />
   <KeyPosition
           motion:framePosition="75"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.6"
           motion:percentX="0.1"
   />
</KeyFrameSet>

Once you're done exploring KeyPosition , in the next step you'll move on to other types of KeyFrames .

Building dynamic animations often means changing the size , rotation , or alpha of views as the animation progresses. MotionLayout supports animating many attributes on any view using a KeyAttribute .

In this step, you will use KeyAttribute to make the moon scale and rotate. You will also use a KeyAttribute to delay the appearance of the text until the moon has almost completed its journey.

Step 1: Resize and rotate with KeyAttribute

  1. Open xml/step5.xml which contains the same animation you built in the last step. For variety, this screen uses a different space picture as the background.
  2. To make the moon expand in size and rotate, add two KeyAttribute tags in the KeyFrameSet at keyFrame="50" and keyFrame="100"

bbae524a2898569.png

step5.xml

<!-- TODO: Add KeyAttributes to rotate and resize @id/moon -->

<KeyAttribute
       motion:framePosition="50"
       motion:motionTarget="@id/moon"
       android:scaleY="2.0"
       android:scaleX="2.0"
       android:rotation="-360"
/>
<KeyAttribute
       motion:framePosition="100"
       motion:motionTarget="@id/moon"
       android:rotation="-720"
/>

These KeyAttributes are applied at 50% and 100% of the animation. The first KeyAttribute at 50% will happen at the top of the arc, and causes the view to be doubled in size as well as rotate -360 degrees (or one full circle). The second KeyAttribute will finish the second rotation to -720 degrees (two full circles) and shrink the size back to regular since the scaleX and scaleY values default to 1.0.

Just like a KeyPosition , a KeyAttribute uses the framePosition and motionTarget to specify when to apply the KeyFrame , and which view to modify. MotionLayout will interpolate between KeyPositions to create fluid animations.

KeyAttributes support attributes that can be applied to all views. They support changing basic attributes such as the visibility , alpha , or elevation . You can also change the rotation like you're doing here, rotate in three dimensions with rotateX and rotateY , scale the size with scaleX and scaleY , or translate the view's position in X, Y, or Z.

Step 2: Delay the appearance of credits

One of the goals of this step is to update the animation so that the credits text doesn't appear until the animation is mostly complete.

  1. To delay the appearance of credits, define one more KeyAttribute that ensures that alpha will remain 0 until keyPosition="85" . MotionLayout will still smoothly transition from 0 to 100 alpha, but it will do it over the last 15% of the animation.

step5.xml

<!-- TODO: Add KeyAttribute to delay the appearance of @id/credits -->

<KeyAttribute
       motion:framePosition="85"
       motion:motionTarget="@id/credits"
       android:alpha="0.0"
/>

This KeyAttribute keeps the alpha of @id/credits at 0.0 for the first 85% of the animation. Since it starts at an alpha of 0, this means it will be invisible for the first 85% of the animation.

The end effect of this KeyAttribute is that the credits appear towards the end of the animation. This gives the appearance of them being coordinated with the moon settling down in the right corner of the screen.

By delaying animations on one view while another view moves like this, you can build impressive animations that feel dynamic to the user.

Try it out

  1. Run the app again and go to Step 5 to see the animation in action. When you click on the moon, it'll follow the path from start to end, going through each KeyAttribute that was specified in the KeyFrameSet .

2f4bfdd681c1fa98.gif

Because you rotate the moon two full circles, it will now do a double back flip, and the credits will delay their appearance until the animation is almost done.

Explore on your own

Before you move on to the final type of KeyFrame , try modifying other standard attributes in the KeyAttributes . For example, try changing rotation to rotationX to see what animation it produces.

Here's a list of the standard attributes that you can try:

  • android:visibility
  • android:alpha
  • android:elevation
  • android:rotation
  • android:rotationX
  • android:rotationY
  • android:scaleX
  • android:scaleY
  • android:translationX
  • android:translationY
  • android:translationZ

Rich animations involve changing the color or other attributes of a view. While MotionLayout can use a KeyAttribute to change any of the standard attributes listed in the previous task, you use a CustomAttribute to specify any other attribute.

A CustomAttribute can be used to set any value that has a setter. For example, you can set the backgroundColor on a View using a CustomAttribute . MotionLayout will use reflection to find the setter, then call it repeatedly to animate the view.

In this step, you will use a CustomAttribute to set the colorFilter attribute on the moon to build the animation shown below.

5fb6792126a09fda.gif

Define custom attributes

  1. To get started open xml/step6.xml which contains the same animation you built in the last step.
  2. To make the moon change colors, add two KeyAttribute with a CustomAttribute in the KeyFrameSet at keyFrame="0" , keyFrame="50" and keyFrame="100".

214699d5fdd956da.png

step6.xml

<!-- TODO: Add Custom attributes here -->
<KeyAttribute
       motion:framePosition="0"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFFFFF"
   />
</KeyAttribute>
<KeyAttribute
       motion:framePosition="50"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFB612"
   />
</KeyAttribute>
<KeyAttribute
       motion:framePosition="100"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFFFFF"
   />
</KeyAttribute>

You add a CustomAttribute inside a KeyAttribute . The CustomAttribute will be applied at the framePosition specified by the KeyAttribute .

Inside the CustomAttribute you must specify an attributeName and one value to set.

  • motion:attributeName is the name of the setter that will be called by this custom attribute. In this example setColorFilter on Drawable will be called.
  • motion:custom*Value is a custom value of the type noted in the name, in this example the custom value is a color specified.

Custom values can have any of the following types:

  • Цвет
  • Integer
  • Float
  • String
  • Dimension
  • Boolean

Using this API, MotionLayout can animate anything that provides a setter on any view.

Try it out

  1. Run the app again and go to Step 6 to see the animation in action. When you click on the moon, it'll follow the path from start to end, going through each KeyAttribute that was specified in the KeyFrameSet .

5fb6792126a09fda.gif

When you add more KeyFrames , MotionLayout changes the path of the moon from a straight line to a complex curve, adding a double backflip, resize, and a color change midway through the animation.

In real animations, you'll often animate several views at the same time controlling their motion along different paths and speeds. By specifying a different KeyFrame for each view, it's possible to choreograph rich animations that animate multiple views with MotionLayout .

In this step you'll explore using OnSwipe with complex paths. So far, the animation of the moon has been triggered by an OnClick listener and runs for a fixed duration.

Controlling animations that have complex paths using OnSwipe , like the moon animation you've built in the last few steps, requires understanding how OnSwipe works.

Step 1: Explore OnSwipe behavior

  1. Open xml/step7.xml and find the existing OnSwipe declaration.

step7.xml

<!-- Fix OnSwipe by changing touchAnchorSide →

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="bottom"
/>
  1. Run the app on your device and go to Step 7 . See if you can produce a smooth animation by dragging the moon along the path of the arc.

When you run this animation, it doesn't look very good. After the moon reaches the top of the arc, it starts jumping around.

ed96e3674854a548.gif

To understand the bug, consider what happens when the user is touching just below the top of the arc. Because the OnSwipe tag has an motion:touchAnchorSide="bottom" MotionLayout will try to make the distance between the finger and the bottom of the view constant throughout the animation.

But, since the bottom of the moon doesn't always go in the same direction, it goes up then comes back down, MotionLayout doesn't know what to do when the user has just passed the top of the arc. To consider this, since you're tracking the bottom of the moon, where should it be placed when the user is touching here?

56cd575c5c77eddd.png

Step 2: Use the right side

To avoid bugs like this, it is important to always choose a touchAnchorId and touchAnchorSide that always progresses in one direction throughout the duration of the entire animation.

In this animation, both the right side and the left side of the moon will progress across the screen in one direction.

However, both the bottom and the top will reverse direction. When OnSwipe attempts to track them, it will get confused when their direction changes.

  1. To make this animation follow touch events, change the touchAnchorSide to right .

step7.xml

<!-- Fix OnSwipe by changing touchAnchorSide →

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="right"
/>

Step 3: Use dragDirection

You can also combine dragDirection with touchAnchorSide to make a side track a different direction than it normally would. It's still important that the touchAnchorSide only progresses in one direction, but you can tell MotionLayout which direction to track. For example, you can keep the touchAnchorSide="bottom" , but add dragDirection="dragRight" . This will cause MotionLayout to track the position of the bottom of the view, but only consider its location when moving right (it ignores vertical motion). So, even though the bottom goes up and down, it will still animate correctly with OnSwipe .

  1. Update OnSwipe to track the moon's motion correctly.

step7.xml

<!-- Using dragDirection to control the direction of drag tracking →

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="bottom"
       motion:dragDirection="dragRight"
/>

Try it out

  1. Run the app again and try dragging the moon through the entire path. Even though it follows a complex arc, MotionLayout will be able to progress the animation in response to swipe events.

5458dff382261427.gif

MotionLayout can be used to build rich animations when used with CoordinatorLayout . In this step, you'll build a collapsible header using MotionLayout .

Step 1: Explore the existing code

  1. To get started, open layout/activity_step8.xml .
  2. In layout/activity_step8.xml , you see that a working CoordinatorLayout and AppBarLayout is already built.

activity_step8.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
       ...>
   <com.google.android.material.appbar.AppBarLayout
           android:id="@+id/appbar_layout"
           android:layout_width="match_parent"
           android:layout_height="180dp">
       <androidx.constraintlayout.motion.widget.MotionLayout
               android:id="@+id/motion_layout"
               ... >
           ...
       </androidx.constraintlayout.motion.widget.MotionLayout>
   </com.google.android.material.appbar.AppBarLayout>
  
   <androidx.core.widget.NestedScrollView
           ...
           motion:layout_behavior="@string/appbar_scrolling_view_behavior" >
           ...
   </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

This layout uses a CoordinatorLayout to share scrolling information between the NestedScrollView and the AppBarLayout . So, when the NestedScrollView scrolls up, it will tell the AppBarLayout about the change. That's how you implement a collapsing toolbar like this on Android—the scrolling of the text will be "coordinated" with the collapsing header.

The motion scene that @id/motion_layout points to is similar to the motion scene in the last step. However, the OnSwipe declaration was removed to enable it to work with CoordinatorLayout .

  1. Run the app and go to Step 8 . You see that when you scroll the text, the moon does not move.

Step 2: Make the MotionLayout scroll

  1. To make the MotionLayout view scroll as soon as the NestedScrollView scrolls, add motion:minHeight and motion:layout_scrollFlags to the MotionLayout .

activity_step8.xml

<!-- Add minHeight and layout_scrollFlags to the MotionLayout -->

<androidx.constraintlayout.motion.widget.MotionLayout
       android:id="@+id/motion_layout"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       motion:layoutDescription="@xml/step8"
       motion:motionDebug="SHOW_PATH"
       android:minHeight="80dp"
       motion:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"  >
  1. Run the app again and go to Step 8 . You see that the MotionLayout collapses as you scroll up. However, the animation does not progress based on the scroll behavior yet.

Step 3: Move the motion with code

  1. Open Step8Activity.kt . Edit the coordinateMotion() function to tell MotionLayout about the changes in scroll position.

Step8Activity.kt

// TODO: set progress of MotionLayout based on an AppBarLayout.OnOffsetChangedListener

private fun coordinateMotion() {
    val appBarLayout: AppBarLayout = findViewById(R.id.appbar_layout)
    val motionLayout: MotionLayout = findViewById(R.id.motion_layout)

    val listener = AppBarLayout.OnOffsetChangedListener { unused, verticalOffset ->
        val seekPosition = -verticalOffset / appBarLayout.totalScrollRange.toFloat()
        motionLayout.progress = seekPosition
    }

    appBarLayout.addOnOffsetChangedListener(listener)
}

This code will register a OnOffsetChangedListener that will be called every time the user scrolls with the current scroll offset.

MotionLayout supports seeking its transition by setting the progress property. To convert between a verticalOffset and a percentage progress, divide by the total scroll range.

Try it out

  1. Deploy the app again and run the Step 8 animation. You see that MotionLayout will progress the animation based on the scroll position.

ee5ce4d9e33a59ca.gif

It's possible to build custom dynamic collapsing toolbar animations using MotionLayout . By using a sequence of KeyFrames you can achieve very bold effects.

This codelab covered the basic API of MotionLayout .

To see more examples of MotionLayout in practice, check out the official sample . And be sure to check out the documentation !

Учить больше

MotionLayout supports even more features not covered in this codelab, like KeyCycle, which lets you control paths or attributes with repeating cycles, and KeyTimeCycle, which lets you animate based on clock time. Check out the samples for examples of each.

For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page .