Getting started with ConstraintLayout in Kotlin - Part 4: using MotionLayout for animations
You will need Android Studio 3+ installed on your machine. Familiarity with Android development will be helpful. You should have completed the previous parts of the series.
In the previous part of this series, we talked about helpers available to you when using ConstraintLayout
. In this part, we will focus on using animations when working with ConstraintLayout
s.
We all love animations, but building animations on Android appears daunting at first. Many times we have to settle for less than what we want or none at all. Before now, Android already offered various ways of implementing animations in our apps:
- View Animations: these were used specifically to animate the visual properties of views like opacity and transparency. This is the initial way animations were implemented on Android.
- Property Animations: this helps us to alter properties of
view objects
to perform various view animations. View properties that can be altered include its translate, scale, and rotate properties. - Drawable Animations: this uses an XML file to specify a list of drawables and runs them one after another to make an animation.
- Layout Transitions: this enables us to easily implement fade/move/resize animations when items are added to or removed from a
ViewGroup
, usually with just one line of code. - ConstraintLayout with
[ConstraintSet](https://developer.android.com/reference/android/support/constraint/ConstraintSet)
s - this gives us the ability to animate between two sets of constraints through the TransitionManager). TheTranstionManager
helps manage transitions when there is a change of scene. A scene represents either the entire user interface or a subset of the layout represented by aViewGroup
.
Prerequisites
For you to follow along in the entire series, you need to have the following requirements:
- Completed previous parts of the series.
- Android Studio (v3.0 or higher) installed on your machine. Download here.
- Ability to navigate the Android Studio IDE.
- Ability to use the layout editor provided by the Android Studio IDE.
- A basic understanding of Android development, especially layouts.
Let’s get started.
The MotionLayout
Compared to the other methods of creating animations on Android, MotionLayout
offers us a lot more flexibility in specifying animations for our apps.
A MotionLayout
is actually a subclass of the ConstraintLayout
and allows you to make animations between two sets of constraints. As we already know, constraints are the building blocks for the layout, and every view must have constraints.
The MotionLayout
is also fully declarative. This means you can easily describe, in your XML
file, how a transition should occur without any Java or Kotlin code. It can also animate any property of the system, not just layout attributes.
As shown in the image below, the background color of the info button crossfades between the image being animated:
Finally MotionLayout
supports touch events and keyframes. This makes it possible to easily customize transitions to your own needs. In later parts of this article, we will see how this works using some examples.
Difference between ConstraintLayout and MotionLayout
Although MotionLayout
is a part of ConstraintLayout
, there are some key differences. One key difference between ConstraintLayout
and MotionLayout
, at the XML
level, is that the description of what MotionLayout
will do is not necessarily contained in the same layout file. It is instead kept in a separate XML
file, a MotionScene
, that it references. This description will take precedence over the description in the layout file.
This approach is very helpful as the layout file will contain only the views and their properties and not their positioning or movement.
The MotionLayout
is only available as part of the ConstraintLayout
version 2.0 and above. As at the time of writing this article, this version of the library is still in its alpha stage.
To add support for MotionLayout
on your project, add the following code to your app modules build.gradle
file:
dependencies {
// [...]
implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha1'
// [...]
}
Sync your gradle files after that to make the library available for use.
Making a simple animation with MotionLayout
Before diving deep into making animations with MotionLayout
, it is important we understand the purpose of the motion. The material design guideline says:
Motion provides meaning. Objects are presented to the user without breaking the continuity of experience even as they transform and reorganize.
For our very first animation with MotionLayout
, we are going to try to achieve the animation below. The ImageView
increases in size as it moves to the bottom of the layout.
In order to achieve this animation, we have to create two layout files, one for the initial position of the image and another for the final position of the image.
For the initial position of the image, our layout looks like this:
This layout was named: motion_one_img_start
. And this is how the XML
code looks like:
<!-- File: app/res/layout/motion_one_img_start.xml -->
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_android_black_24dp" />
</android.support.constraint.ConstraintLayout>
Next, let’s create the Android vector drawable. To do so, make sure the app
directory in the Android file list is selected then click File > New > Vector Asset then set the hex color to #4CAF50
then click Next > Finish.
For our final image position our layout looks like this:
This layout was named motion_one_img_end
and this is the supporting XML
code:
<!-- File: app/res/layout/motion_one_img_end.xml -->
<android.support.constraint.ConstraintLayout
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">
<ImageView
android:id="@+id/imageView"
android:layout_width="180dp"
android:layout_height="180dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75"
app:srcCompat="@drawable/ic_android_black_24dp" />
</android.support.constraint.ConstraintLayout>
Now that we have defined our initial and final position for our images, we need to create a MotionLayout
layout file, we will call it motion_layout_01
. This file will be very similar to our initial layout file except that instead of the root layout being a ConstraintLayout
we will replace it with a MotionLayout
.
Here are the changes:
<!-- File: app/res/layout/motion_layout_01.xml -->
<android.support.constraint.motion.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:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_scene_01"
app:showPaths="true">
<ImageView
android:id="@+id/imageView"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_android_black_24dp" />
</android.support.constraint.motion.MotionLayout>
We also introduced two new attributes that we didn’t declare in our initial layout file. The app:layoutDescription
and app:showPaths
attributes. The showPaths
attribute was used here to show the animation path in our example. This attribute is mainly for debugging and should not be used in a production app.
The layoutDescription
attribute references an XML
file called motion_scene_01
. It is the MotionScene
element that will tell the MotionLayout
how to transition between the initial layout and final layout. All MotionScene
files should be kept in res/xml
directory:
Create a new file in the res/xml
directory named motion_scene_01
and paste this:
<!-- File: app/res/xml/motion_scene_01.xml -->
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@layout/motion_one_img_start"
motion:constraintSetEnd="@layout/motion_one_img_end"
motion:duration="1000">
<OnClick
motion:target="@+id/imageView"
motion:mode="toggle" />
</Transition>
</MotionScene>
Here we defined the default transition by specifying the constraintSetStart
, constraintSetEnd
and duration
attributes.
- The
constraintSetStart
attribute tellsMotionLayout
the constraints for the initial position of the layout filemotion_one_img_start
we created earlier. - The
constraintSetEnd
attribute tellsMotionLayout
the constraints for the final position of the layout filemotion_one_img_end
we created earlier. - The
duration
attribute specifies the duration of the transition between the initial and final position.
Finally to we used the OnClick
event with a handler to instruct MotionLayout
to start its transition. On the OnClick
we specified the ID of the view that triggers the animation using the target
attribute and the mode
attribute.
The mode
attribute specifies the direction for the target view to move the animation. We used the toggle
mode here so we can achieve a smooth back and forth transition between the initial and final position. Other available modes include: transitionToEnd
, transitionToStart
, jumpToEnd
, jumpToStart
.
Finally, open the MainActivity
class and change the setContentView
parameter from R.layout.activity_main
to R.layout.motion_layout_01
in the onCreate
method.
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.motion_layout_01)
}
}
We can now run our app. When we click on the image it will transition between the initial and final positions as seen in our GIF below:
Deeper dive into MotionScene
As mentioned before, the MotionScene
drives the animation for MotionLayout
by instructing the MotionLayout
what to do. It is the engine room of our animations.
To specify an animation, the MotionScene
element could contain:
- A
StateSet
element that describes the states supported by the system. Astate
can be used to define the position of a layout before, during, and after the transition. - A
ConstraintSet
that encapsulates all the positioning rules for your layout. It is always important to make sure that eachConstraintSet
element contains all the constraints you want to apply to the view. It’s important because each constraint set will replace all existing constraints of the affected views. - A
Transition
element that describes the transition between twostates
orConstraintSet
s. Under the transition element, you can also specify event triggers likeOnClick
orOnSwipe
and aKeyFrameSet
.
Below is a typical structure of a MotionScene
file:
<MotionScene>
<Transition>
<OnClick />
<OnSwipe />
<KeyFrameSet >
<KeyPosition />
<KeyAttribute />
<KeyCycle />
</KeyFrameSet>
</Transition>
<ConstraintSet>
<Constraint >
<CustomAttribute/>
</Constraint>
</ConstraintSet>
<StateSet>
<State>
<Variant />
</State>
</StateSet>
</MotionScene>
Using ConstraintSet to replace Multiple Layout files
While making our first animation with MotionLayout
above, we defined two layout files. One for the initial position, and one for the final position. An alternative to doing this is we can specify what constraints the initial and final layout positions will use directly in our MotionScene
file.
To do this we use the ConstraintSet
element and define the initial constraints and the final constraints to be applied during the transition.
Let’s look at the example below of how it could look:
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="1000">
<OnClick
motion:target="@+id/imageView"
motion:mode="toggle" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/imageView"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="16dp"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:srcCompat="@drawable/ic_android_black_24dp"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/imageView"
android:layout_width="180dp"
android:layout_height="180dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintVertical_bias="0.75"
motion:srcCompat="@drawable/ic_android_black_24dp" />
</ConstraintSet>
</MotionScene>
Above, we have defined two ConstraintSet
elements. One for our initial layout with an ID - start
, and one for our final layout with ID - end
.
If we compare this to the animation we created earlier, we see that the constraint attributes for our start
constraint set is similar to the constraint declared for the ImageView
in our initial layout position in the file motion_one_img_start
earlier. The same goes for our end ConstraintSet
attributes.
Finally instead of constraintSetStart
and constraintSetEnd
referencing the initial and final layouts we now reference the ids
of the ConstraintSet
for the initial and final constraint declared in our MotionScene
file.
One major reason to use a single file to manage our constraints is future-proofing. The upcoming MotionEditor
in Android Studio will likely only support self-contained MotionScene
files.
Custom attributes
Earlier, we mentioned that with MotionLayout
we can perform transitions on attributes that are not related to the position only - these are called custom attributes.
One example of such an attribute is the background color:
In the image above, you can see how the background color gradually changes during the animation and not just an abrupt change. Let’s see an example of how we can achieve this.
First create a new XML file in the res/layouts
directory named motion_layout_02.xml
and paste this:
<!-- File: app/res/layout/motion_layout_02.xml -->
<android.support.constraint.motion.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:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_scene_02"
app:showPaths="true">
<View
android:id="@+id/view"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.motion.MotionLayout>
A key thing to notice is that the view element we are going to apply the background color on has no backgroundColor
attribute. We will instead declare this in our MotionScene
file.
For the contents of our MotionScene
, create a new file named motion_scene_02
in the res/xml
directory and paste this:
<!-- File: app/res/xml/motion_scene_02.xml -->
<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:interpolator="linear">
<OnClick
motion:target="@+id/view"
motion:mode="toggle" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/view"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="8dp"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#000000" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/view"
android:layout_width="180dp"
android:layout_height="180dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintVertical_bias="0.90" >
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#0e0e96" />
</Constraint>
</ConstraintSet>
</MotionScene>
When defining a custom attribute, you need to define it both at the start and end ConstraintSet
. A custom attribute is specified with an attributeName
- this is case sensitive and must match the getter/setter methods of an object such that:
- getter - getAttributeName (e.g getBackgroundColor)
- setter - setAttributeName (e.g setBackgroundColor)
The value type of the setter also needs to be specified. The following value types are supported:
customColorValue
customIntegerValue
customFloatValue
customStringValue
customDimension
customBoolean
We can now update the MainActivity
to use the motion_layout_02
and run our app to see the animation replicated.
KeyFrames
So far we have only made transitions between two states - the start and end state. Sometimes we want the start state to pass through intermediary states before arriving at the end state as seen below:
This is the same animation we implemented in our first example. The difference here is that before arriving at the final state, the image moves to the left and rotates at an angle before reaching the final state.
To achieve this sort of animation, MotionLayout
offers us keyframes. Keyframes allow us to specify a point on the timeline of the animation where we can make additional changes to the animation.
In the example above, we specified that at 50% of the animation timeline, the image should move to the left of the screen and rotate before arriving at the final state. To do this we will need to add the KeyFrameSet
element to our MotionScene
. Let’s consider how we would do this.
Open the motion_scene_01
file and add the KeyFrameSet
between the Transition
tag as seen below:
<!-- File: app/res/xml/motion_scene_01.xml -->
[...]
<Transition
motion:constraintSetStart="@layout/motion_one_img_start"
motion:constraintSetEnd="@layout/motion_one_img_end"
motion:duration="1000">
[...]
<KeyFrameSet>
<KeyPosition
motion:type="parentRelative"
motion:percentX="0.25"
motion:framePosition="50"
motion:target="@+id/imageView"/>
<KeyAttribute
android:rotation="-45"
motion:framePosition="50"
motion:target="@id/imageView" />
</KeyFrameSet>
</Transition>
[...]
To make a position change during the transition, we need to add a KeyPosition
element under the KeyFrameSet
. Above we specify the type
and the direction, which is percentX
for the x-axis, percentY
for the y-axis.
To make an attribute change during the transition, we add a KeyAttribute
element to the KeyFrameSet
. We can add any attribute
of the view
, widget
or layout
that we want to change here. For this example, we only want to change the rotation of the ImageView
.
Finally, the framePosition
attribute is used to specify at what time during the animation the changes should be applied. Notice we did not need to change anything in our MotionLayout
. We only specified the changes to be made in our MotionScene
file.
Here is what that file now contains:
<!-- File: app/res/xml/motion_scene_01.xml -->
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="1000">
<OnClick
motion:target="@+id/imageView"
motion:mode="toggle" />
<KeyFrameSet>
<KeyPosition
motion:type="parentRelative"
motion:percentX="0.25"
motion:framePosition="50"
motion:target="@+id/imageView"/>
<KeyAttribute
android:rotation="-45"
motion:framePosition="50"
motion:target="@id/imageView" />
</KeyFrameSet>
</Transition>
</MotionScene>
Limitations of MotionLayout
With great power comes great responsibility. However, so far MotionLayout
will only provide its capabilities for its direct children . TransitionManager
mentioned earlier can work with nested layout hierarchies as well as Activity transitions.
Conclusion
In this article, we learned how to make a simple animation using MotionLayout
. We also learned how the motion scene file helps MotionLayout
drive animations. Making animations in Android has never been easier. Can’t wait to see what crazy animations you do with MotionLayout
.
In the final part of this series, we will concatenate all the knowledge we learned in all previous parts and build an application using ConstraintLayout
.
You can find the repository for this project here.
13 September 2018
by Neo Ighodaro