Chapters

Hide chapters

Real-World Android by Tutorials

First Edition · Android 10 · Kotlin 1.4 · AS 4

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

12. MotionLayout & Motion Editor
Written by Subhrajyoti Sen

Animations and transitions are a great way of improving your app’s user experience. Android has a wide set of classes you can use to implement different kinds of animations, but historically, using them to create anything complex has been difficult. Fortunately, Android introduced MotionLayout in ConstraintLayout 2.0 to address these problems.

MotionLayout makes it possible to implement detailed animations entirely in XML, similar to the way you create layouts. In this chapter, you’ll learn how to use MotionLayout to

  • Animate view dimensions.
  • Translate views.
  • Preview the animation in the IDE.
  • Change the shape of images and apply filters.

Getting to know MotionLayout

Before you start creating beautiful animations with MotionLayout, you need to learn about its three main concepts:

  1. MotionScene: This is the root element for every animation scene. It contains the different states of the animation and the transitions between them.
  2. ConstraintSet: A collection of Constraint tags. A Constraint is a set of ConstraintLayout attributes that you apply to a specific view. Typically, you’ll have two ConstraintSets that define the start and end states of the animation. Although you can have more ConstraintSets in theory, XML only lets you use two. If you need to use more than two states in the animation, you have to do that programmatically.
  3. Transition: Defines the transition between two ConstraintSets. You can also set properties, like the animation duration and the interpolator, to change the values of the constraints.

This is in the context of ConstraintLayout, where you represent the state of a specific View, or a group of Views, as the set of the constraints you apply to them. Different constraints produce a different state for the Views. You then use a Transition to represent how you go from one state to another.

Finally, as Figure 12.1 shows, a MotionScene is a way to aggregate different states for a View and the way you transition from one to another. This produces an animation.

Figure 12.1 — The Concept Behind MotionScene
Figure 12.1 — The Concept Behind MotionScene

Getting started

Open the starter project from the downloaded materials and run it, then go to the details page of any pet. You’ll notice that the layout of the page is a bit different from what you implemented in the previous chapter. This change adds scrollable content, which lets you create gesture-based animations.

Figure 12.2 — The Starter Project
Figure 12.2 — The Starter Project

Open the project build.gradle and verify that your constraint_layout_version is at least 2.0.0.

To create your first MotionScene, you need to:

  1. Create an XML resource with the MotionScene.
  2. Apply the MotionScene to a specific View.

Note: A ViewGroup is a specific View that aggregates other Views using the Composite design pattern. How you aggregate the other Views is what defines a specific layout. From now on, anything you read about Views also applies to ViewGroups or in general layouts.

Defining a MotionLayout

You can define a MotionLayout declaratively by using an XML document. Create a file named fragment_details_scene.xml in res/xml and insert the following code:

<MotionScene
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">

  <ConstraintSet android:id="@+id/start"> 
  </ConstraintSet>

  <ConstraintSet android:id="@+id/end"> 
  </ConstraintSet>

  <Transition 
    motion:constraintSetEnd="@+id/end" 
    motion:constraintSetStart="@id/start"
    motion:motionInterpolator="linear"
    motion:duration="1000">
  </Transition>  

</MotionScene>

In this document, you create a MotionScene. In particular:

  1. You use <MotionScene/> as the root element for the XML document. As described in Figure 12.1, you use the <MotionScene/> as the container for the definitions of ConstraintSet and Transition.

  2. Using <ConstraintSet/>, you define a specific state. In this case, you’re just creating the placeholder for what you consider the starting state by setting its ID to start

  3. In the same way, you use <ConstraintSet/> to define the placeholder for the final state of the transition that you identify with the end ID.

  4. Using <Transition/>, you define how the animation should run.

  5. With the constraintSetStart and constraintSetEnd attributes, you bind the Transition to the specific initial and final states. In this case, you’re representing how you go from start to end.

  6. With the previous attributes, you said you want to go from start to end but you didn’t specify how this would happen. Using motionInterpolator, you now set the linear interpolator. This means the rate at which the constraints’ values change stays constant over time. For example, by animating the alpha of a View from 0 to 1 over 200 milliseconds, the alpha will be 0.25 after the initial 50 milliseconds, 0.5 after 100 milliseconds and so on.

  7. Finally, you use duration to define the duration of the transition in milliseconds.

To get a good feeling of how this works, play around with the previous configuration. Try, for instance, other values for the motionInterpolator attribute. You can choose among these values:

  • easeInOut
  • easeIn
  • easeOut
  • linear
  • bounce

How do you choose the right interpolator for your animation? Well, there’s no definite answer to this. Unless you have specific timing in mind, try out a few interpolators and check which one looks best.

Alternatively, you can also specify your own interpolator with something like:

<MotionScene
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- // ... -->
  <Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:motionInterpolator="cubic(.17,.67,.83,.67)" 
    motion:duration="1000">
  </Transition>  

</MotionScene>

Here, you use cubic(x1,y1,x2,y2) to pass coefficients representing the control points of a cubic Bezier from 0,0 to 1,1. Bezier.com has a great interactive tool to get the values for such interpolators. Find it at https://cubic-bezier.com.

Now that you have a MotionScene, you can apply it to a specific View.

Applying MotionScene to a View

Now, you need to apply the MotionScene you just created to a specific View. To do this, first open fragment_details.xml and add the following:

<androidx.constraintlayout.motion.widget.MotionLayout
  android:id="@+id/motion_layout" 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layoutDescription="@xml/fragment_details_scene"> <!-- HERE -->
  <!-- // ... -->
</androidx.constraintlayout.motion.widget.MotionLayout>  

In this XML document, you:

  1. Replaced the root androidx.constraintlayout.widget.ConstraintLayout with androidx.constraintlayout.motion.widget.MotionLayout.
  2. Added app:layoutDescription="@xml/fragment_details_scene", which refers to the MotionScene that MotionLayout needs to transition between states.

As you see in Figure 12.3, you’ll notice that it doesn’t affect the layout preview. That’s because MotionLayout extends from ConstraintLayout and inherits all its features.

Figure 12.3 — Layout Editor With MotionLayout
Figure 12.3 — Layout Editor With MotionLayout

Now that you’ve set up MotionLayout, it’s time to explore its various features and see them in action.

Adding your first constraint

As mentioned above, MotionLayout works by transitioning between two states, where each state is represented by a ConstraintSet. Inside each ConstraintSet, you have multiple Constraints corresponding to different views. You only need to define a Constraint for the views you want to animate, not every view.

You define a Constraint using the id of the view you want to animate and a set of corresponding attributes that change the position and orientation of the views. For example, you can set the height and width of a TextView but not its background.

One important thing to note is that the start and end constraints defined in MotionScene both inherit from the layout defined inside MotionLayout. That means that if you don’t want to change the starting state of a view in the transition, you don’t have to add a Constraint for it in the start ConstraintSet.

For your first animation, you’ll create a transition that shrinks the size of the pet’s image and places it at the top-left corner. To do this, you only need to add a Constraint to the end ConstraintSet.

Open fragment_details_scene.xml and add the following:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- / ...-->
  <ConstraintSet android:id="@+id/end">
    <Constraint
      android:id="@id/image"
      android:layout_width="100dp"
      android:layout_height="100dp"
      android:layout_marginBottom="@dimen/default_margin"
      android:layout_marginStart="@dimen/default_margin"
      android:layout_marginTop="@dimen/default_margin"
      motion:layout_constraintStart_toStartOf="parent"
      motion:layout_constraintTop_toTopOf="parent" />
  </ConstraintSet>
  <!-- / ...-->
</MotionScene>

In this code, you:

  1. Add a <Constraint/> as a child of <ConstraintSet/> with the end ID to indicate it’s the final state.
  2. Add a <Constraint/> for the View with the ID image.
  3. Set the image size to 100dp.
  4. Set some margins.
  5. Constraintthe image to the top and start of the parent.

It’s time to preview the animation and see how it looks, which you’d usually do by building and running the app. Up to now, this has been the only way of previewing animations on Android. However, now there’s a shiny tool that will make the lives of Android developers much easier: Motion Editor.

Motion Editor

Motion Editor is a handy tool that comes built-in with Android Studio 4.0 and later. It lets you preview animations created with MotionLayout without having to leave the IDE. Additionally, it provides a Graphical User Interface to add and edit different ConstraintSets, Constraints, Transitions and much more.

It’s similar to how you can create layouts using both XML and Android Studio’s Design View. For this chapter, you’ll mainly use Motion Editor to preview your animations.

To display Motion Editor, open fragment_details and choose the Split or Design tab near the top-right of the screen.

Figure 12.4 — Layout Editor Tabs
Figure 12.4 — Layout Editor Tabs

Once Motion Editor is open, you’ll see a screen like this:

Figure 12.5 — The Motion Editor
Figure 12.5 — The Motion Editor

The screen shown above has four main components:

  1. The base MotionLayout
  2. The start ConstraintSet
  3. The end ConstraintSet
  4. The Transition

To preview the animation, select Transition. You’ll then see a Timeline window, like the one shown below.

Figure 12.6 — Transition Timeline
Figure 12.6 — Transition Timeline

With this window, you can play or pause the animation, speed it up or slow it down and even preview the animation running both forward and backward.

Click the Play button and you’ll see your animation in action. While the animation is playing, you’ll notice a dashed line on the preview. This is the motion path — it denotes the path the view takes from the start of the transition to the end. Later in the chapter, you’ll use this line to improve the transition.

Figure 12.7 — The Motion Path
Figure 12.7 — The Motion Path

If you can’t view that path, pause the animation midway and it’ll appear. You can also view these paths on a device or emulator by adding the app:showPaths="true" attribute to the MotionLayout tag.

Congratulations, you’ve successfully created your first animation using MotionLayout. Next, you’ll learn how to trigger your animation to start.

Adding a trigger

Animations seldom start on their own; they’re usually associated with an event or user interaction. For example, you click on a button to load something and the button animates to a progress bar. It would be weird for this animation to start on its own.

When you previewed your animation in the Motion Editor, you used a Play button. When the app runs on a device, however, you need to give the user a way to trigger the animation. MotionLayout provides two such triggers:

  1. OnClick: Activates when the user clicks a specific View.
  2. OnSwipe: Activates when the user performs a swipe gesture in a certain direction on a specific View.

In this section, you’ll use the OnSwipe trigger to make the transition start when the user swipes up on the content below the image.

Adding OnSwipe

Open fragment_details_scene.xml and add the <OnSwipe/> element to Transition:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- / ...-->
  <Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="1000"
    motion:motionInterpolator="linear">
    <OnSwipe
      motion:dragDirection="dragUp"
      motion:touchAnchorId="@id/scrollView" />
  </Transition>

</MotionScene>

In the code above, dragDirection specifies the direction of the swipe. The supported drag directions are: up, down, left and right. touchAnchorId specifies the View the user needs to drag.

Build and run. Slowly drag upwards on the content below the pet’s image and you’ll see your animation in action. As a bonus, after the animation, swipe downwards on the same part of the content. You’ll see your animation run in reverse.

Figure 12.8 — The Scene at the End of the Transition
Figure 12.8 — The Scene at the End of the Transition

One of the nice features of MotionLayout is that you can make forward and backward animations work without having to create an explicit transition.

Notice that the Lottie loading animation is visible even after the page has finished loading and some of the pet details display while the page is still loading. This is due to a special property of MotionLayout, which you’ll explore in the next section.

Overriding visibility

MotionLayout controls the visibility of all its child views. Even if you tried to control the visibility of child views programmatically, it wouldn’t have any effect. Luckily, MotionLayout provides functionality to ignore this behavior.

Enter the following code in the start ConstraintSet:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">

  <ConstraintSet android:id="@+id/start">
    <Constraint android:id="@+id/loader">
      <PropertySet motion:visibilityMode="ignore" />
    </Constraint>

    <Constraint android:id="@+id/call">
      <PropertySet motion:visibilityMode="ignore" />
    </Constraint>

    <Constraint android:id="@+id/scrollView">
      <PropertySet motion:visibilityMode="ignore" />
    </Constraint>
  </ConstraintSet>
  <!-- // ... -->
</MotionScene>

visibilityMode="ignore" instructs MotionLayout to not override the visibility of the view with the ID loader.

Build and run the app now and you’ll notice that the dog’s image is no longer visible during the transition and the pet’s details don’t display when the loader is shown.

Figure 12.9 — Transaction Without Loader
Figure 12.9 — Transaction Without Loader

Animating more features

The current transition animates only the pet’s image. How about animating the pet’s name and the Call button as well? In this section, you’ll add constraints to:

  • Change the name’s alignment from center- to left-justified while increasing its size.
  • Make the Call button roll off the screen.

To do this, add the following code to the end ConstraintSet:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- // ... -->
  <ConstraintSet android:id="@+id/end">
    <!-- // ... -->
    <Constraint
      android:id="@+id/call"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginBottom="@dimen/default_margin"
      android:rotation="180"
      motion:layout_constraintBottom_toBottomOf="parent"
      motion:layout_constraintStart_toEndOf="parent" />

    <Constraint
      android:id="@+id/name"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginBottom="@dimen/default_margin"
      android:layout_marginStart="@dimen/default_margin"
      android:layout_marginTop="@dimen/default_margin"
      android:scaleX="1.4"
      android:scaleY="1.4"
      motion:layout_constraintStart_toStartOf="parent"
      motion:layout_constraintTop_toBottomOf="@+id/image" />
  </ConstraintSet>
  <!-- // ... -->
</MotionScene>

In the code above, the first Constraint adds a rotation of 180 degrees to the call view and also constrains the start of the view to the end of the parent. That places it at the right of the window and hides it from the user. This will make it seem like the view is rotating out of the screen.

The second Constraint aligns the name view to the start of the parent and scales it to 1.4 times its original size. It also adds a start margin to align the view properly with the rest of the text.

Build and run. You’ll observe that, as you slowly drag up, the Call button rolls out of the screen and the pet’s name moves diagonally to the left while increasing in size.

Figure 12.10 — Animation With Multiple Constraints
Figure 12.10 — Animation With Multiple Constraints

Adding non-linear motion

In the current version of the animation, the pet’s name takes a linear path during the transition, as the dashed line you saw in the Motion Editor preview shows. The path line is straight, denoting linear animation. However, the transition would look much better with a curved path.

MotionScene uses the concept of frames. Each frame denotes an instant in the transition. The first frame has a position of 0, while 100 denotes the final position.

MotionScene provides multiple ways to specify frame properties. The two most important are:

  1. KeyAttribute: Specifies attributes of a view in the frame.
  2. KeyPosition: Specifies the position of the view in the frame. You can define the position relative to the parent or the path or define it as a delta of the distance covered by the view over the entire transition.

The supported KeyAttributes are:

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

You define KeyAttribure and KeyPosition inside a KeyFrameSet. To do this, add the following code to the Transition:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- // ... -->
  <Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="1000"
    motion:motionInterpolator="linear">
    <KeyFrameSet>
      <KeyPosition
        motion:framePosition="50"
        motion:keyPositionType="parentRelative"
        motion:motionTarget="@id/name"
        motion:percentX="0.4" />
    </KeyFrameSet>
    <!-- // ... -->
  </Transition>
</MotionScene>

In the code above, you use <KeyPosition/> to define a frame that applies to the name view and is midway in the transition by giving it a framePosition of 50.

You use percentX of 0.4 and keyPositionType of parentRelative to specify that, at frame position 50, the view should cover 40% of the distance along the X-axis instead of the 50% it would cover otherwise. This gives a curved path to the motion, which you can verify using the path line in the preview.

Figure 12.11 — Curved Motion Path
Figure 12.11 — Curved Motion Path

To make it easier to see the changes while the animation is running, use Motion Editor’s speed toggle to select an animation speed of 0.25x.

Figure 12.12 — Transition Speed
Figure 12.12 — Transition Speed

ImageFilterView

In addition to MotionLayout, ConstraintLayout 2.0 also introduced a utility class named ImageFilterView, which extends AppCompatImageView and makes it easy to apply filters to images. Now, you no longer need to include a new third-party library to get a circular ImageView. With ImageFilterView, you get out-of-the-box support to change the radius of the image, crossfade between two images, change the image saturation and, much more.

In the current transition, the pet’s image only shrinks in size and moves to the top-left corner. In this section, you’ll modify the transition so the image transforms from a square to a circular image as it moves toward the top.

That might sound complex to implement, but you’ll soon see that the combination of ImageFilterView and MotionLayout makes it quite simple.

CustomAttribute

Look closely at all the view attributes you’ve animated so far and you’ll notice that these are attributes that apply to any View or affect the positions of the different views. It’s not possible to assign a custom property in any Constraint.

That’s because MotionLayout provides a custom tag named CustomAttribute to use with attributes that are either unrelated to the position or are specific to certain views.

For example, you can set the android:src attribute of an ImageView or the android:backgroundColor of a Button. You define CustomAttribute with the name of the attribute and its value.

To try this out, open fragment_details.xml and replace the id image ImageView with the following ImageFilterView:

<androidx.constraintlayout.motion.widget.MotionLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/motion_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layoutDescription="@xml/fragment_details_scene">
  <!-- // ... -->
  <androidx.constraintlayout.utils.widget.ImageFilterView
    android:id="@+id/image"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:contentDescription="@string/image_of_pet"
    android:scaleType="centerCrop"
    tools:src="@drawable/cute_doggo"
    app:layout_constraintDimensionRatio="H,1:1"
    app:layout_constraintTop_toTopOf="parent"
    app:roundPercent="0" />
  <!-- // ... -->
</androidx.constraintlayout.motion.widget.MotionLayout>

Remember to use the fully-qualified name for ImageFilterView: androidx.constraintlayout.utils.widget.ImageFilterView.

Next, open fragment_details_scene.xml and insert the following code inside the start ConstraintSet:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">

  <ConstraintSet android:id="@+id/start">
    <!-- / ... -->
    <Constraint
      android:id="@+id/image"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:contentDescription="@string/image_of_pet"
      motion:layout_constraintTop_toTopOf="parent"
      motion:layout_constraintDimensionRatio="H,1:1">
      <CustomAttribute
        motion:attributeName="roundPercent"
        motion:customFloatValue="0"/>
      <CustomAttribute
        motion:attributeName="saturation"
        motion:customFloatValue="1"/>
    </Constraint>
  </ConstraintSet>
  <!-- / ... -->
</MotionScene>

In this code, attributeName specifies the name of the attribute and customFloatValue specifies its value. There are separate attributes for different value types, like customStringValue when the custom attribute takes a string input or customBoolean when the input value must be a Boolean.

roundPercent specifies the corner radius of the image. A value of 0 represents a rectangular image while 1 represents a circular image.

Similarly, saturation specifies the, well, saturation, of the image with 1 representing the image with its original saturation and 0 representing a monochrome image.

Now, insert the following code inside the image constraint in the end ConstraintSet in the same file:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- / ... -->
  <ConstraintSet android:id="@+id/end">
    <!-- / ... -->
    <CustomAttribute
      motion:attributeName="roundPercent"
      motion:customFloatValue="1"/>
    <CustomAttribute
      motion:attributeName="saturation"
      motion:customFloatValue="0"/>
  </ConstraintSet>
  <!-- / ... -->
</MotionScene>

Switch over to Motion Editor and play the animation. You’ll see it morphs from being rectangular and colorful to circular and monochrome.

That was simple to implement, wasn’t it? Without using MotionLayout, this would require a lot of complicated Kotlin code and multiple other libraries.

Figure 12.13 — Image Filter and Transformation
Figure 12.13 — Image Filter and Transformation

Congratulations, you’ve successfully implemented a set of complex animations, all through XML and without having to deploy your to a device multiple times. This is the true beauty of MotionLayout.

Key points

  • MotionLayout is an extension of ConstraintLayout that lets you write complex animations in a declarative way through XML.
  • You can use Motion Editor to preview animations without leaving your IDE.
  • A Transition defines the start and end state of the motion as well as properties like the motion’s duration.
  • A ConstraintSet defines a state in the transition. It consists of a collection of Constraints for each view that you’ll animate.
  • A KeyFrameSet specifies attributes and locations of views at distinct points in the transition.
  • CustomAttribute sets attribute values that are either View properties or are unrelated to the position.
  • Use ImageFilterView to apply common filters to images and also change properties like the radius.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.