MotionLayout Tutorial For Android: Getting Started

Learn how to use the new ConstraintLayout subclass MotionLayout to add effects such as translation animations and alpha/color changes.

Version

  • Kotlin 1.2, Android 4.1, Android Studio 3

Meaningful motion is an important aspect of every modern application. Using animations, transitions, and text or view transformations, you can improve the UX (user experience) of your application and make your users happy. Unfortunately, this is not always simple. Android provides many different ways to animate objects on the screen and it’s always a challenge to find the right one. Sometimes you imagine complex animations, but you have to give up implementing them because of the time requirements.

Now you can solve all these problems with MotionLayout!

MotionLayout was been announced by Google at Google I/O 2018 and it’s a solution for mitigating the difficulty of creating complex animations. It provides a declarative, smart and clean way of building animations, without a single line of code.

As you’ll see below, MotionLayout extends ConstraintLayout, and so inherits all of its features. Through the internal use of ConstraintLayout and its helpers, MotionLayout allows you to animate color changes, alpha transitions and views, using as little as a single XML file.

In this tutorial you’ll see how you can combine a few animations, without much work, using MotionLayout. You’ll learn how to:

  • Animate alpha changes
  • Translate views
  • Interpolate color changes
  • Combine MotionLayout with other Android components
  • Note: This tutorial assumes you’ve got good knowledge of Android development, and that you’ve had previous experience with animations in Android apps. If you’re new to Android, please check out our Beginner Android series and the Android Animations tutorial

    Getting Started

    Before you begin, download the tutorial materials from the Download Materials button at the top or the bottom of the page. You’ll see the starter and final projects, which you’ll use throughout the tutorial.

    Next, open up the build.gradle file under the app folder, and change the ConstraintLayout dependency version to 2.0.0-alpha2:

dependencies {
  - -
  // Support Libraries
  implementation "com.android.support:appcompat-v7:$support_lib_version"
  implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha2'
    - - -
  }

Finally, open up the onboarding_view.xml file, and change the root layout’s type to MotionLayout.

  <android.support.constraint.motion.MotionLayout 
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:id="@+id/onboardingRoot"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

     - - -
  </android.support.constraint.motion.MotionLayout>

The goal of this tutorial is to create an on-boarding screen custom View, which will have a few animations. You’ll animate the next, skip and finish buttons, as well as the background color, like so:

In order to do this you need to understand what MotionScene, KeyFrameSet and the KeyAttribute are and how to use them. So let’s get started! :]

Introducing MotionLayout

If you look at the build.gradle file you updated earlier, you’ll see that there is no dependency for MotionLayout. This is because the MotionLayout is actually extends ConstraintLayout and the constraint-layout-2.0.0-alpha2 dependency includes everything you need.

MotionLayout uses a mechanism called the MotionScene to create all of the underlying animations. It is similar to Scene definitions which you can write to animate between screen states. They also define states in which a screen, and its elements, can be. Each of the scenes can have Transition, KeyFrameSet and ConstraintSet definitions. Let’s look at what each of them does.

MotionScene

With MotionLayout you can create your animations in a declarative way through the definition of a MotionScene in an XML file in the res/xml folder. This is where you can define everything the framework needs in order to run your animations. Thanks to the nature of ConstraintLayout, you can define the different states of the View components in the animation using different Constraint definitions, into a ConstraintSet element.

Animating doesn’t mean just moving objects but, in general, changing their properties in a time dependent fashion. You can move from state A to state B in many different ways with the definition of a Transition. Even if you know what your initial and final states are, you also need to define how this should happen and what are the possible intermediate states. This can be done using a KeyFrameSet definition.

It’s important to note that you define the MotionLayout in a file that is different from the one that contains the layout. You can apply the same MotionScene to different layout definitions, enhancing reusability. Applying an animation to a layout is as easy as assigning its reference as the value of the app:layoutDescription XML attribute of the MotionLayout element in the layout.

Transition

The name here is pretty self-explanatory. A Transition describes a change from state A to state B. You configure the start constraint set using the app:constraintSetStart attribute and the final one using app:constraintSetEnd. Then, when the progress of the Motionscene increases, the layout starts the transformation to the end constraint. Progress is just a number, ranging from 0 to 100, saying what percentage of the animation has currently been played.

The total duration of the animation is usually split in 100 different parts which are played, one after the other, at constant rate. If you need a different distribution in time of the animation, you need a custom interpolator that you can then set using the app:interpolator XML attribute.

KeyFrameSet

You can think of any animation as a sequence of frames you render on the screen during a given interval of time. If you want to animate a specific property from the value A to the value B, the framework chooses, by default, the shortest way. For instance, if A and B are positions on the screen, you’ll see the items moving through a straight line with constant speed.

You’ve just learned that if you want to change the way the frames are displayed in time you can use a different interpolator, but what if you want to change the path? Using the KeyFrameSet element, it’s possible to add some intermediate constraints that must be satisfied at some given times. For instance, you can tell the system to go from A to B through a path which contains C at time 20 and D at time 80.

To do this you can use the KeyAttribute or CustomAttribute elements, depending on what you need. You can use the former in case of more common View properties like position or alpha, but in the case of a component with custom properties, you need the latter. If you use CustomAttributes, you can use a custom String identifier, but it has to match the set method on the view. A good example is backgroundColor, as you can call setBackgroundColor() on a view.

ConstraintSet

Finally, you use ConstraintSet definitions to define starting and final constraints, for each of the Views you want to animate. It’s important to note that a MotionScene doesn’t need to define a ConstraintSet for every View in the layout, but just for those that should be animated.

That being said, if you don’t provide an end constraint for a view, it will disappear. This happens because the library doesn’t know which constraints it should apply at the end of the animation. However, if you do provide start and end constraints, the views will move from the start to the end, or they won’t move at all if the start constraints match the end ones.

Working with MotionLayout

To start off working with MotionLayout, you first have to create a MotionScene in an XML file. Head over to res/xml, right-click it and choose New > XML resource file, using onboarding_scene as the name.

Next, prepare the content of the file using the MotionScene XML element like in the following snippet:

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

Note: Currently the XML editor doesn’t work well with motion scenes, and all the tags you can use with it. Sometimes you won’t have autocomplete, so make sure to follow the code snippets from the tutorial.

Before you head out to animations, first you have to define the constraints for all the views you’ll be animating. Open up the onboarding_view.xml file, to see the layout structure that you’ll animate. There’s the Previous, Next and Finish buttons, the background, and a view pager with an indicator. If you launch the application you’ll see something like this:

The initial layout

Don’t worry if the Next and Finish buttons are overlapping on the bottom right part of the screen: you’ll fix everything very soon.

Our idea is to animate the previous button in, when you’ve moved away from the first page. Additionally, you should simultaneously animate the finish and next buttons, so that the finish button moves into the screen, while the next button moves out of the screen, when you swipe to the last page.

Thinking about this, you don’t have to change the constraints of any views. You can simply translate them in and out, or change the views’ alpha.

So let’s start by defining the start and end constraints.

Defining ConstraintSets

Each MotionScene should have two sets of constraints: a starting one and a final one. Usually, the former will be whatever you declared in the layout file, and the latter will define how you want the layout to change. When you don’t have to change the constraints, you can have both of the sets nearly the same.

To add a set, you just create a ConstraintSet XML element. In it, you can create as many constraints as you want, although they usually depend on the number of views in the layout.

Inside the onboarding_scene.xml file you created earlier, add this ConstraintSet definition as a child of the MotionScene

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

    <Constraint
      android:id="@id/onboardingRoot"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

    <Constraint
      android:id="@id/previousButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent" />

    <Constraint
      android:id="@id/nextButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" />

    <Constraint
      android:id="@id/finishButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" />
  </ConstraintSet>

These are the starting constraints you use in the layout file. Also notice that the set has an id. You use this to differentiate the start from the end constraints. You’ll create a transition and connect these later on.

Now, you’ll add the end version of the constraints. These should be the same, so you can copy and paste the last snippet, but change the id to @+id/end. It should look like this:

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

    <Constraint
      android:id="@id/onboardingRoot"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

    <Constraint
      android:id="@id/previousButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent" />

    <Constraint
      android:id="@id/nextButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" />

    <Constraint
      android:id="@id/finishButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" />
  </ConstraintSet>

Now that you have the constraints ready, all that you have to do to is to apply this MotionScene to the layout file.

Open up onboarding_view.xml again, and add the following attribute to the layout root: app:layoutDescription="@xml/onboarding_scene". The MotionLayout tag should now look like this:

<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/onboardingRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/onboarding_scene">

 - - - 
</android.support.constraint.motion.MotionLayout>

Finally, you have to connect these two sets using a Transition. Add the following snippet to the top of the onboarding_scene.xml file:

<Transition
    app:constraintSetEnd="@id/end"
    app:constraintSetStart="@id/start" />

Build and run the application and you should see the initial layout. And if you look to the bottom-right corner of the app, to the Next and Finish buttons, you’ll see that they still overlap. This is because you’re still missing the animations. You’ll worry about that in a moment, when you start animating views that move them around.

Animating attributes

You haven’t really animated anything yet, so let’s change that. There are a few things you can animate in a MotionLayout. First off, you’ll add the button animations, so they don’t overlap, and they appear at the right time. Additionally, to make all the animations run you have to setup progress changes with the ViewPager.

Open up the OnboardingView class in the view package, and go to the onPageScrolled function. It looks something like this:

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    if (numberOfPages > 1) {
      val newProgress = (position + positionOffset) / (numberOfPages - 1)

      //update progress
    }
  }

All you have to do to change the progress of the MotionScene is to add the following line under the comment:

onboardingRoot.progress = newProgress

So whenever you scroll the pager, you will update the animation’s progress.

As mentioned before, you will make the Previous button appear once you leave the first page. So a good way to do this is to set its alpha to 0 at the start, and change it to 1 in the end. Go back to the onboarding_scene.xml file. Add the android:alpha="0" attribute to the previousButton start constraint.

<ConstraintSet android:id="@+id/start">
  - - - 
  <Constraint
    android:id="@id/previousButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:alpha="0"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />
  - - - 
</ConstraintSet>

Also add android:alpha="1" to the end constraint counterpart.

<ConstraintSet android:id="@+id/end">
  - - - 
  <Constraint
    android:id="@id/previousButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:alpha="1"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />
  - - - 
</ConstraintSet>

Build and run the code. You should see the Previous button fade in and out. You’ve made your first animation! :]

But you can do better. Rather than changing alpha from 0 to 1 across all the pages, it would be better if it went to full alpha when you leave the first page. To do this, you have to use a KeyFrame. You have to tell MotionLayout to go to full alpha in 20% of the animation – when you scroll to the second page.

Defining animation progress

Once again, open up the onboarding_scene.xml file. Add the following code snippet within the Transition XML tag:

<KeyFrameSet>
  <KeyAttribute
    android:alpha="0"
    app:framePosition="0"
    app:target="@id/previousButton" />

  <KeyAttribute
    android:alpha="1"
    app:framePosition="20"
    app:target="@id/previousButton" />
</KeyFrameSet>

This snippet makes the animation go from no alpha to full alpha, in the first 20% of the progress. Once the View reaches full alpha, it will stay full, unless you go back to the first page, where it will fade out. Simple as that. You can build and run the project again, and it should work smoothly now.

How about you solve those overlapping buttons now?

Changing constraints

One of the best things MotionLayout allows you to do is change constraints. Without using much code, you can translate your UI all around, and build fun motion constructs. But there is a problem. You can’t define constraint framePosition, like you did with the alpha. Let’s see how you can solve the problem of overlapping buttons, with constraints.

Change the nextButton and finishButton start and end constraints to the following values.

Start:

  <Constraint
    android:id="@id/nextButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

  <Constraint
    android:id="@id/finishButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toEndOf="parent" />

And end:

  <Constraint
    android:id="@id/nextButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

  <Constraint
    android:id="@id/finishButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

With these changes you moved the finishButton start at the end of the screen. When the progress changes, it will start moving inside the layout, and the nextButton will move outside of the same layout.

Build and run the project, and you should see the buttons moving in a linear manner, rather than on a specific page.

Because of this constraint, you will instead move the buttons using the translationX and translationY properties. You’ll see how in the next section.

Translating Buttons

Because of the issue previously described, you’ll translate the Next button out of the layout, and simultaneously move the Finish button in.

Undo the last changes you made, regarding constraint sets, in the onboarding_scene.xml file. You now need to define start and end states in the ConstraintSets for the new translation values.

First, add the following tags to the Next and Finish buttons: android:translationY="0dp" to Next and android:translationX="70dp" to Finish for the starting constraints, and android:translationY="50dp" to Next and android:translationX="0dp" to Finish for the ending constraints.

Build and run the app, and you should see the buttons move as you move through pages.

Once again, you have to define steps for the animations. Since the change has to happen at the very last page, you’ll have to keep the translation up to about the 75% of the animation. Add the following snippet of code, within the KeyFrameSet:

  <KeyAttribute
    android:translationY="0dp"
    app:framePosition="75"
    app:target="@id/nextButton" />

  <KeyAttribute
    android:translationY="50dp"
    app:framePosition="100"
    app:target="@id/nextButton" />

  <KeyAttribute
    android:translationX="70dp"
    app:framePosition="75"
    app:target="@id/finishButton" />

  <KeyAttribute
    android:translationX="0dp"
    app:framePosition="100"
    app:target="@id/finishButton" />

If you build and run the app again, you’ll see the same animations at play. However, they will run only when switching between the last two pages.

Your onboarding is starting to look pretty good. :]

Animating custom attributes

To finish off the on-boarding, you will animate a custom attribute of the OnBoardingView. Your pages are pretty blank and you’ll add a color interpolation between them. Now, this might sound complicated, but in reality, it’s not. Moreover, you can achieve it using only KeyAttributes, as well.

Add the following code to the onboarding_scene.xml file within the Transition tag:

  <KeyFrameSet>

    <KeyAttribute
      app:framePosition="20"
      app:target="@id/background"
      app:transitionEasing="accelerate">
      <CustomAttribute
        app:attributeName="backgroundColor"
        app:customColorValue="#A5C63A" />
    </KeyAttribute>

    <KeyAttribute
      app:framePosition="40"
      app:target="@id/background"
      app:transitionEasing="accelerate">
      <CustomAttribute
        app:attributeName="backgroundColor"
        app:customColorValue="#C3E6E6" />
    </KeyAttribute>

    <KeyAttribute
      app:framePosition="60"
      app:target="@id/background"
      app:transitionEasing="accelerate">
      <CustomAttribute
        app:attributeName="backgroundColor"
        app:customColorValue="#464E69" />
    </KeyAttribute>

    <KeyAttribute
      app:framePosition="80"
      app:target="@id/background"
      app:transitionEasing="accelerate">
      <CustomAttribute
        app:attributeName="backgroundColor"
        app:customColorValue="#AF6DA2" />
    </KeyAttribute>

    <KeyAttribute
      app:framePosition="100"
      app:target="@id/background"
      app:transitionEasing="accelerate">
      <CustomAttribute
        app:attributeName="backgroundColor"
        app:customColorValue="#FBBF1D" />
    </KeyAttribute>
  </KeyFrameSet>

There’s quite a bit of code, but it follows a common pattern. For each step, in your case each page, you define an attribute you want to change. Of course, all the attributes – background color and the target – are the same. The only thing that changes is the framePosition and the backgroundColor. Custom attributes work using the set function, matching the attribute name from the View. So, as you’re swiping, the setBackgroundColor function will be called, with the calculated value.

Build and run the app, and you should see the pages brought to life. You now have a fully-working onboarding process, which is nice and smooth. :]

Defining view transitions

You still haven’t used many Transition options for the onboarding screen. Since they don’t really fit in there, you’ll learn more about transitions and their properties on a separate screen.

First, right-click on the xml folder and create the transition_scene.xml file, just like you did before for the other scene. Then, paste the following code inside:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">

  <Transition
    app:constraintSetEnd="@id/end"
    app:constraintSetStart="@id/start">

    <OnSwipe
      app:touchAnchorId="@id/happyAndroid"
      app:touchAnchorSide="top" />
  </Transition>

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

    <Constraint
      android:id="@id/happyAndroid"
      android:layout_width="100dp"
      android:layout_height="100dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  </ConstraintSet>

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

    <Constraint
      android:id="@id/happyAndroid"
      android:layout_width="100dp"
      android:layout_height="100dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent" />
  </ConstraintSet>
</MotionScene>

I’ll explain what’s going on in this XML code below.

Additionally, just like you did before, change the root layout to MotionLayout in the activity_transition xml, and add app:layoutDescription="@xml/transition_scene" as an attribute.

Finally, you have to switch the activity which launches after the splash screen.

Open up the SplashActivity.kt file, and navigate to the following code:

Handler().postDelayed({

  // Start activity
  startActivity(Intent(this, MainActivity::class.java))

  // Animate the loading of new activity
  overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)

  // Close this activity
  finish()

}, 2000)

Change the target activity for the startActivity function to TransitionActivity::class.java. That line of code should look like this now:

  startActivity(Intent(this, TransitionActivity::class.java))

Now build and run the application. If you try swiping the Bugdroid image, it should move from the top to the bottom:

So how does this animation work? The magic is in the OnSwipe XML tag. It creates a standard swipe handler which anchors the touch start from the top of the happyAndroid element. When you swipe, the Bugdroid will move between the start and end constraints.

Although the Bugdroid can bounce up and down, we can make him a bit more interesting, by adding KeyPositions. Open up the transition_scene.xml file again, and add the following code within the Transition tag:

<KeyFrameSet>
  <KeyPosition
    app:framePosition="20"
    app:keyPositionType="parentRelative"
    app:percentX="0.2"
    app:percentY="0.3"
    app:target="@id/happyAndroid" />

  <KeyPosition
    app:framePosition="50"
    app:keyPositionType="parentRelative"
    app:percentX="0.8"
    app:percentY="0.7"
    app:target="@id/happyAndroid" />
</KeyFrameSet>

By adding KeyPositions, you changed the linear path for the Bugdroid. It will now move in a spiral, relative to the parent’s percentage positions. To see what this looks like, build and run the app. You should see something like this:

The Bugdroid has some moves now! Each KeyPosition is like a checkpoint through which the view passes. They can have an X-axis percentage and a Y-axis percentage, which can be relative to the path or the parent View. You can learn more about this in a tutorial by Google.

Where to go from here

MotionLayout is still some ways from being stable. Once tools like the KeyFrame editor come out, it will be much easier to build and preview animations. However, this doesn’t mean that you can’t start building awesome animations with MotionLayout.

Check out these articles and links which also explain how to use the new API, and what you can build with it:

Contributors

Comments