MotionLayout is a library that lets you add rich motion into your Android app. It's based upon ConstraintLayout and lets you animate anything that you can build using ConstraintLayout.

You can use MotionLayout to animate the location, size, visibility, alpha, color, elevation, rotation, and other attributes of multiple views at the same time. This lets you create coordinated animations involving multiple views through declarative XML that are difficult to achieve in code.

MotionLayout animations extend many of the same concepts as Keyframe animations with Constraint Layout to allow you to finely customize the animation.

In this codelab, you will learn the basics of MotionLayout and how to use it to build rich animations in your app.

Why do we animate?

Animations are a great way to enhance an app experience. You can use animations to:

What you'll learn

Learn more about Constraint Layout

To use MotionLayout you need to be familiar with Constraint Layout. Read through the Constraint Layout codelab to learn more about constraint layout.

To download the sample app, you can either:

Download ZIP

... or clone the GitHub repository from the command line by using the following command:

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

Frequently asked questions

For this step we will build an animation that moves a view from the top start of the screen to the bottom end in response to user clicks. To define an animation, MotionLayout needs to know the start and end of the animation.

After you complete this step, you'll have implemented the following animation.

Introducing the motion scene

MotionLayout is a subclass of Constraint Layout, so it supports all of the same features. To use MotionLayout, you just add a MotionLayout view where you would use ConstraintLayout

If you open up activity_step1.xml you'll find a MotionLayout with a single ImageView inside of it.

<!-- initial code -->
<!-- activity_step1.xml -->
<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       >

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

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

This will behave the same as a full screen ConstraintLayout, but as of yet we haven't added any constraints. If you ran the app now, you would see an empty screen and the star would not be displayed.

To animate constraints you need to specify a motion scene for the MotionLayout to use. This will let us describe the animation we want to build.

To update our layout to point to a motion scene, add an app:layoutDescription attribute to the MotionLayout and point it to @xml/step1

<!-- add app:layoutDescription="@xml/step1" -->

<androidx.constraintlayout.motion.widget.MotionLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:layoutDescription="@xml/step1">

Declaring a motion scene

To define the motion scene, create an XML file at res/xml/step1.xml that has a MotionScene tag:

<?xml version="1.0" encoding="utf-8"?>
<!-- xml/scene1.xml -->

<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:android="http://schemas.android.com/apk/res/android"> 
    <!-- TODO: Define a Transition -->
    
    <!-- TODO: Define @id/start -->

    <!-- TODO: Define @id/end -->
</MotionScene>

Start and End constraints

All animations can be defined in terms of a start and an end. It specifies what things should look like before the animation and after the animation. MotionLayout will take this start and end and figure out how to fluidly animate between the states.

You define the start and the end using the same constraints used in Constraint Layout. MotionLayout uses a ConstraintSet tag to define the constraints. A ConstraintSet is what it sounds like, a set of constraints that can be applied to views. This includes width, height, and Constraint Layout constraints. It also includes some attributes such as alpha. It doesn't contain the views themselves, just the constraints on those views.

Any constraints specified in a ConstraintSet will override the constraints specified in the layout file. If you define constraints in both the layout and the Motion Scene, only the constraints in the Motion Scene are applied.

In this case, let's constrain the star view to the top start of the screen to start, and the bottom end of the screen at the end.

In XML, replace the TODO for @id/start with the following ConstraintSet.

<!-- xml/scene1.xml -->
<!-- TODO: Define @id/start -->

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

The ConstraintSet has an ID of @id/start, and any constraints in this constraint set are children of the ConstraintSet.

In this animation, since there is only one view, there is only one Constraint in the ConstraintSet. The Constraint specifies the id that it's constraining, @id/red_star which points to the view that was defined in activity_step1.xml. It's important to note that Constraint tags specify only constraints and layout information - the Constraint tag doesn't know that it's being applied to an ImageView.

The constraint specifies the height, width, and constraints needed to constrain the view to the top start of the parent.

Now let's specify the ConstraintSet for the end of our animation:

<!-- xml/scene1.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"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

Just like @id/start, this ConstraintSet has a single Constraint on @id/red_star. This time it constrains it to the bottom end of the screen.

Define a Transition

Every MotionScene must have at least one transition. A transition defines every part of one animation, completely describing the entire transition from one state to another.

A transition must specify a start and end ConstraintSet for the transition. These constraints are applied to the views at the start and the end of the animation. While the animation is running, MotionLayout will figure out a path between the start and end for every view.

In addition, a transition can specify how to modify the animation in other ways, such as how long to run the animation or how to animate by dragging views.

Note: It is possible to load a ConstraintSet from an existing ConstraintLayout by pointing start or end to a layout file instead of a ConstraintSet. However, we don't recommend this as it's harder to keep the code in sync between multiple files.

Replace the TODO to define a transition:

<!-- xml/scene1.xml -->
<!-- TODO: Define a Transition -->

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

This is everything that MotionLayout needs to build an animation. Looking at each attribute:

MotionLayout will then figure out a path between the start and end constraints and animate it for the specified duration.

The transition also tells MotionLayout to run the animation in response to click events using an <OnClick> tag. Looking at each attribute:

Animations in action

If you run the app again, you will see your animation is running when you click on the star.

The completed motion scene file defines one Transition which points to a start and end ConstraintSet.

At the start of the animation (@id/start), the star icon is constrained to the top start of the screen. At the end of the animation (@id/end) the star icon is constrained to the bottom end of the screen.

<?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
           app:constraintSetStart="@+id/start"
           app:constraintSetEnd="@+id/end"
           app:duration="2000">
       <!-- MotionLayout will handle clicks on @id/star to "toggle" the animation between the start and end -->
       <OnClick
               app:targetId="@id/red_star"
               app: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"
               app:layout_constraintStart_toStartOf="parent"
               app: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"
               app:layout_constraintEnd_toEndOf="parent"
               app:layout_constraintBottom_toBottomOf="parent" />
   </ConstraintSet>
</MotionScene>

For this step we will build an animation that responds to a user touch event to start the animation. Motion layout supports tracking touch events to move views, as well as physics-based fling gestures to make the motion fluid.

After you complete this step, you'll have implemented the following animation.

Open the existing code

To get started, open up activity_step2.xml, where you'll find an existing MotionLayout.

<!-- initial code in activity_step2.xml -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       app: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"
           ...
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

This layout defines all of the views for the animation. The three star icons are not constrained in the layout because they will be animated in the motion scene.

The credits TextView does have constraints applied, because it stays in the same place for the entire animation and doesn't modify any attributes.

Animating the scene

Open up the motion scene xml/step2.xml to define the animation.

Just like the last animation, the animation will be defined by a start and end ConstraintSet and a Transition.

Define the start ConstraintSet

At the start, all three stars are centered in the bottom of the screen. The right and left star have an alpha value of 0.0, which means that they're hidden.

<!-- xml/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"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintBottom_toBottomOf="parent" />

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

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

In this ConstraintSet specify one Constraint for each of the stars. Each constraint will be applied by MotionLayout at the start of the animation.

Each view is centered at the bottom of the screen using start, end, and bottom constraints. The two stars @id/left_star and @id/right_star both have an additional alpha value that will be applied at the start of the animation.

Note: The start and end constraint sets define the start and end of the animation. A constraint on the start, like app:layout_constraintStart_toStartOf will constrain a view's start to the start of another view. This can be confusing at first because the name start is used for both and they're both used in the context of constraints. To help draw out the distinction, the start in layout_constraintStart refers to the "start" of the view, which is the left in a left to right language and right in a right to left language. The start constraint set refers to the start of the animation..

Define the end ConstraintSet

The end constraint set uses a chain to position all three stars together below @id/credits. In addition, it will set the alpha of the left and right star to 1.0.

<!-- xml/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"
           app:layout_constraintHorizontal_chainStyle="packed"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toStartOf="@id/red_star"
           app:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           app:layout_constraintStart_toEndOf="@id/left_star"
           app:layout_constraintEnd_toStartOf="@id/right_star"
           app: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"
           app:layout_constraintStart_toEndOf="@id/red_star"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintTop_toBottomOf="@id/credits" />
</ConstraintSet>

The end result is that the views will spread out from the center as they animate up.

In addition, since the alpha property is set on @id/right_start and @id/left_star in both ConstraintSets, both views will fade in as the animation progresses.

Animating based on user swipe

MotionLayout can track user swipe events to create a physics-based "fling" animation. This means the views will keep going if the user flings them and will slow down like a physical object would when rolling across a surface. You can add this type of animation with an OnSwipe tag in the Transition.

Replace the TODO for adding an OnSwipe tag with <OnSwipe app:touchAnchorId="@id/red_star" />.

<!-- xml/step2.xml -->
<!-- TODO add OnSwipe tag -->

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

OnSwipe contains a few attributes, the most important being touchAnchorId.

When MotionLayout listens to swipe, the listener will be registered on the MotionLayout view and not the touchAnchorId. When a user starts a swipe gesture anywhere on the screen, MotionLayout will keep the distance between their finger and the touchAnchorSide of the touchAnchorId view. If they touch 100dp away from the anchor side, for example, MotionLayout will keep that side 100dp away from their finger for the entire animation.

Try it out

Run the app again, and open the Step 2 screen. You will see the animation. Try "flinging" or releasing your finger halfway through the animation to explore how MotionLayout displays fluid physics based animations!

MotionLayout can animate between very different designs using the features from Constraint Layout to create rich effects.

In this animation, all three views are positioned relative to their parent at the bottom of the screen to start. At the end, the three views are positioned relative to @id/credits in a chain.

Despite these very different layouts, MotionLayout will create a fluid animation between start and end.

For this step we will build an animation that follows a complex path during the animation. MotionLayout can modify the path a view will take between the start and the end using a KeyPosition.

After you complete this step, you'll have implemented the following animation driven by clicking on the moon.

Explore the existing code

Open up layout/activity_step3.xml and xml/step3.xml to see the existing layout and motion scene.

In the layout file (layout/activity_step3.xml), an ImageView and TextView are created to display the moon and credits text.

In the motion scene file (xml/step3.xml), a Transition from @id/start to @id/end is defined. The animation moves the moon image from the lower left of the screen to the bottom right of the screen using two ConstraintSets. The credits text fades in from alpha="0.0" to alpha="1.0" as the moon is moving.

If you run the app right now, you'll see that the moon follows a linear path (or a straight line) from start to end when you click on the moon. The code is pretty much the same as what we did in the previous steps.

Enabling path debugging

Before we add an arc to the moon's motion, it's helpful to enable path debugging in MotionLayout.

To help develop complex animations with MotionLayout, you can draw the animation path of every view. This is helpful when you want to visualize your animation at a glance, and for fine tuning the small details of motion.

To enable debugging paths, open up layout/activity_step3.xml and add app:motionDebug="SHOW_PATH" to the MotionLayout tag.

<!-- layout/activity_step3.xml -->
<!-- Add app:motionDebug="SHOW_PATH" -->

<androidx.constraintlayout.motion.widget.MotionLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:background="@drawable/sh_two_oneseventy"
       app:layoutDescription="@xml/step3"
       app:motionDebug="SHOW_PATH" >

After you enable path debugging, when you run the app again you'll see the paths of all views visualized with a dotted line.

In this animation, the middle circle is the position of the credits text, for example.

Modifying a path

All animations in MotionLayout are defined by a start and end ConstraintSet. This defines what the screen should look like before the animation starts and after the animation is done. By default, MotionLayout will plot a linear path (or a straight line) between the start and end position of each view that changes position.

To build complex paths like the arc of the moon in this example, MotionLayout uses a KeyPosition to modify the path that a view takes between the start and end.

Open up xml/scene3.xml to add a KeyPosition to the scene!

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

A KeyFrameSet is a child of a Transition, and it is a set of all the KeyFrames, such as KeyPosition, that should be applied during the transition.

As MotionLayout is calculating the path for the moon between the start and the end, it modifies the path based on the KeyPosition specified in the KeyFrameSet. You can see how this modifies the path by running the app again.

A KeyPosition has several attributes that describe how it modifies the path, the most important ones are:

You can think of it this way, "At framePosition modify the path of motionTarget by moving it by percentX or percentY according to the coordinates determined by keyPositionType."

By default MotionLayout will round any corners that are introduced by modifying the path. If you look at the animation we just created, you can see the moon follows a curved path at the bend. For most animations, this is what you want, and if not you can specify the curveFit attribute to customize it.

Understanding KeyPosition types

The attribute keyPositionType determines how MotionLayout will modify the path according to percentX or percentY.

There are three different types possible: parentRelative, pathRelative, and deltaRelative. Specifying a type will change the coordinate system by which percentX and percentY are calculated.

ParentRelative coordinates

The keyPositionType of parentRelative uses the same coordinate system as the screen. It defines (0, 0) to the top right of the entire MotionLayout, and (1, 1) to the bottom left.

You can use parentRelative whenever you want to make an animation that moves through the entire MotionLayout - like the moon arc in this example.

However, if you want to modify a path relative to the motion, for example make it curve just a little bit, the other two coordinate systems are a better choice.

DeltaRelative coordinates

In deltaRelative coordinates, (0,0) is the starting position of the view, and (1,1) is the ending position. The X and Y axes is aligned with the screen.

The X axis is always horizontal on the screen, and the Y axis is always vertical on the screen. Compared to parentRelative, the main difference is that the coordinates describe just the part of the screen that the view will be moving.

deltaRelative is a great coordinate system for controlling the horizontal or vertical motion in isolation. For example, you could create an animation that completes just its vertical (Y) movement at 50%, and continues animating horizontally (X).

PathRelative coordinates

The last coordinate system in MotionLayout is pathRelative. It's quite different than the other two - the X axis follows the motion path from start to end. So (0,0) is the starting position, and (1,0) is the ending position.

Why would you want this? It's quite surprising at first glance - and it isn't even aligned to the screen coordinate system.

It turns out pathRelative is really useful for a few things.

  1. Speeding up, slowing down, or stopping a view during part of the animation. Since the X dimension will always match the path the view takes exactly, you can use a pathRelative KeyPosition to change which framePosition a particular point in that path is reached. So a KeyPosition at framePosition="50" with a percentX="0.1" would cause the animation to take 50% of the time to travel the first 10% of the motion.
  2. Adding a subtle arc to a path. Since the Y dimension is always perpendicular to motion, changing Y will change the path to curve relative to the overall motion.
  3. Adding a second dimension when deltaRelative won't work. For completely horizontal and vertical motion, deltaRelative will only create one useful dimension. However, pathRelative will always create usable X and Y coordinates.

Try it out

If you run the app again, you'll see the animation for this step.

The moon follows an arc because it goes through a KeyPosition specified in the Transition.

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

You can read this KeyPosition as, "At framePosition 50 (halfway through the animation) modify the path of motionTarget @id/moon by moving it by 50% Y (halfway down the screen) according to the coordinates determined by parentRelative (the entire MotionLayout)."

So, half way through the animation, the moon must go through a KeyPosition that's 50% down on the screen. This KeyPosition doesn't modify the X motion at all, so the moon will still go from start to end horizontally. MotionLayout will figure out a smooth path that goes through this KeyPosition and while moving between start and end.

If you look closely, the credits text is constrained by the position of the moon. Why isn't it moving vertically as well?

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

It turns out, even though we're modifying the path that the moon takes, the start and end positions of the moon don't move it vertically at all. The KeyPosition doesn't modify the start or the end position, so the credits text is constrained to the final end position of the moon.

If you did want the credits to move with the moon, you could add a KeyPosition to the credits, or modify the start constraints on @id/credits.

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

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

In this step, we will modify the motion to round out the motion of the moon to create a more "moon like" arc.

To get started open up xml/step4.xml which has the same views and the KeyFrame you added in the last step.

Modifying 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 we're building an arc, but you could make it jump up and down in the middle of the screen if you wanted.

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. After you've added these KeyPositions you can see the path by running the app again in the emulator.

<!-- xml/scene3.xml -->
<!-- TODO: Add two more KeyPositions to the KeyFrameSet here -->
<KeyPosition
       app:framePosition="25"
       app:motionTarget="@id/moon"
       app:keyPositionType="parentRelative"
       app:percentY="0.6"
/>
<KeyPosition
       app:framePosition="75"
       app:motionTarget="@id/moon"
       app:keyPositionType="parentRelative"
       app: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 out the animation

Run the app again, and you'll see the animation in action. When you click on the moon, it'll follow 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 KeyFrames, 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.

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

Once your done exploring KeyPosition, in the next step we'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 KeyAttributes 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 it's journey.

To get started 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.

After you have completed this step, you will have created the following animation.

Resize and Rotate with KeyAttribute

To make the moon expand in size and rotate, add two KeyAttributes in the KeyFrameSet at keyFrame="50" and keyFrame="100"

<!-- xml/step5.xml -->

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

<!-- KeyAttributes modify attributes during motion -->
<KeyAttribute
       app:framePosition="50"
       app:motionTarget="@id/moon"
       android:scaleY="2.0"
       android:scaleX="2.0"
       android:rotation="-360"
/>
<KeyAttribute
       app:framePosition="100"
       app: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.

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 we'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.

Delay the appearance of credits

If you look closely at the animation we're trying to build in this step, the credits text doesn't appear until the animation is mostly complete. 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.

<!-- xml/step5.xml -->

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

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

Since this KeyAttribute keeps the alpha of @id/credits at 0.0 for the first 85% of the animation. Note, that 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 the credits appear to the user 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 out the animation

Run the app again, and you'll 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.

Because we 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:

Rich animations involve changing the color or other attributes of a view. While MotionLayout can use a KeyAttribute to change any of the attributes that apply to any view, to specify any other attribute a motion scene can specify a CustomAttribute.

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 reflections 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 this animation.

To get started open xml/step5.xml which contains the same animation you built in the last step.

Defining custom attributes

To make the moon change colors, add two KeyAttribute with a CustomAttribute in the KeyFrameSet at keyFrame="50" and keyFrame="100"

<!-- xml/step6.xml -->

<!-- TODO: Add Custom attributes here -->

<KeyAttribute
       app:framePosition="50"
       app:motionTarget="@id/moon">
   <CustomAttribute
       app:attributeName="colorFilter"
       app:customColorValue="#FFB612"
   />
</KeyAttribute>
<KeyAttribute
       app:framePosition="100"
       app:motionTarget="@id/moon">
   <CustomAttribute
           app:attributeName="colorFilter"
           app:customColorValue="#FFFFFF"
   />
</KeyAttribute>

A CustomAttribute is added inside a KeyAttribute and will be applied at the framePosition specified by the KeyAttribute.

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

Custom values can have any of the following types:

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

Try out the animation

Run the app again, and you'll 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.

By adding more KeyFrames MotionLayout will change 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 run for a fixed duration.

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

Exploring OnSwipe behavior

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

<!-- xml/step7.xml -->

<!-- Fix OnSwipe by changing touchAnchorSide -->
<OnSwipe
       app:touchAnchorId="@id/moon"
       app:touchAnchorSide="bottom"
/>

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

Try running it on your device and see if you can produce a smooth animation by dragging the moon.

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 app: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 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. Which side should it go to?

Using 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 @id/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.

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

<!-- xml/step7.xml -->

<!-- Fix OnSwipe by changing touchAnchorSide -->
<OnSwipe
       app:touchAnchorId="@id/moon"
       app:touchAnchorSide="right"
/>

Using dragDirection

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

This OnSwipe will also track the moon's motion correctly.

<!-- xml/step7.xml -->

<!-- Using dragDirection to control the direction of drag tracking -->
<OnSwipe
       app:touchAnchorId="@id/moon"
       app:touchAnchorSide="bottom"
       app:dragDirection="dragRight"
/>

Try out the animation

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.

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

To get started, open up layout/activity_step8.xml.

After you complete this step you'll have built this animation.

Explore the existing code

In layout/activity_step8.xml a working CoordinatorLayout and AppBarLayout is already built.

<!-- layout/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
           ...
           app: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.

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.

Make the MotionLayout scroll

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

<!-- layout/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"
       android:background="@drawable/m_eightyone"
       app:layoutDescription="@xml/step8"
       app:motionDebug="SHOW_PATH"
       android:minHeight="80dp"
       app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"  >

If you run the app again, you'll see that the MotionLayout collapses as you scroll up. However, the animation does not progress based on the scroll behavior yet.

Move the motion with code

Open Step8Activity.kt to tell MotionLayout about the changes in scroll position.

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

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 it's transition by setting the progress property. To convert between a verticalOffset and a percentage progress, divide by the total scroll range.

Try out the animation

If you run the app again, you'll see that MotionLayout will progress the animation based on the scroll position.

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.

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

Thanks for doing the MotionLayout codelab!