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:
- MotionScene: This is the root element for every animation scene. It contains the different states of the animation and the transitions between them.
-
ConstraintSet: A collection of
Constrainttags. AConstraintis a set ofConstraintLayoutattributes that you apply to a specific view. Typically, you’ll have twoConstraintSets that define the start and end states of the animation. Although you can have moreConstraintSets 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. -
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.
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.
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:
- Create an XML resource with the
MotionScene. - Apply the
MotionSceneto a specificView.
Note: A
ViewGroupis a specificViewthat aggregates otherViews using the Composite design pattern. How you aggregate the otherViews is what defines a specific layout. From now on, anything you read aboutViews also applies toViewGroups 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:
-
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 ofConstraintSetandTransition. -
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 tostart -
In the same way, you use
<ConstraintSet/>to define the placeholder for the final state of the transition that you identify with theendID. -
Using
<Transition/>, you define how the animation should run. -
With the
constraintSetStartandconstraintSetEndattributes, you bind theTransitionto the specific initial and final states. In this case, you’re representing how you go fromstarttoend. -
With the previous attributes, you said you want to go from
starttoendbut you didn’t specify how this would happen. UsingmotionInterpolator, you now set thelinearinterpolator. This means the rate at which the constraints’ values change stays constant over time. For example, by animating the alpha of aViewfrom 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. -
Finally, you use
durationto 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:
easeInOuteaseIneaseOutlinearbounce
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:
- Replaced the root
androidx.constraintlayout.widget.ConstraintLayoutwithandroidx.constraintlayout.motion.widget.MotionLayout. - Added
app:layoutDescription="@xml/fragment_details_scene", which refers to theMotionScenethat 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.
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:
- Add a
<Constraint/>as a child of<ConstraintSet/>with theendID to indicate it’s the final state. - Add a
<Constraint/>for theViewwith the IDimage. - Set the image size to 100dp.
- Set some margins.
- Constraintthe image to the
topandstartof 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.
Once Motion Editor is open, you’ll see a screen like this:
The screen shown above has four main components:
- The base MotionLayout
- The start ConstraintSet
- The end ConstraintSet
- The Transition
To preview the animation, select Transition. You’ll then see a Timeline window, like the one shown below.
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.
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:
-
OnClick: Activates when the user clicks a specific
View. -
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.
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.
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.
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:
- KeyAttribute: Specifies attributes of a view in the frame.
- 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:visibilityandroid:alphaandroid:elevationandroid:rotationandroid:rotationXandroid:rotationYtransitionPathRotateandroid:scaleXandroid:scaleYandroid:translationXandroid:translationYandroid: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.
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.
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.
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
Transitiondefines the start and end state of the motion as well as properties like the motion’s duration. - A
ConstraintSetdefines a state in the transition. It consists of a collection ofConstraints for each view that you’ll animate. - A
KeyFrameSetspecifies attributes and locations of views at distinct points in the transition. -
CustomAttributesets attribute values that are eitherViewproperties or are unrelated to the position. - Use
ImageFilterViewto apply common filters to images and also change properties like the radius.