MotionLayout in action: Creating a simple swipe-to-answer button

In this article, we are going to implement a swipe-to-answer button using MotionLayout. It’s quite simple. Take a look at what we want to achieve:

In case it’s your first time hearing about MotionLayout, MotionLayout is a layout type that helps you manage motion and widget animation in your app. It is a subclass of ConstraintLayout and builds upon its rich layout capabilities. MotionLayout is fully declarative, meaning that you can describe any transitions or animation in XML, no matter how complex.

So, let’s get started…


Adding Dependency

To use MotionLayout in your project, add the ConstraintLayout 2.0 dependency to your app’s build.gradle file.

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
}

Creating the Button Layout

Here, I have created a simple layout which would be our custom button. Like I said in the intro, MotionLayout is a subclass of ConstraintLayout, it has all the capabilities of the ConstraintLayout plus more (animations, transitions, etc). As a result of that, you can easily replace your existing ConstraintLayout with MotionLayout and you’d be good to go.

<androidx.constraintlayout.motion.widget.MotionLayout
    android:layout_width="250dp"
    android:layout_height="32dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    android:id="@+id/button_layout"
    android:background="@drawable/button_layout_bg">

    <TextView
        android:id="@+id/answerText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:paddingEnd="16dp"
        android:paddingRight="16dp"
        android:layout_marginStart="8dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        android:textSize="12sp"
        android:textStyle="bold"
        app:layout_constraintTop_toTopOf="@+id/answerImage"
        app:layout_constraintBottom_toBottomOf="@id/answerImage"
        app:layout_constraintStart_toEndOf="@+id/answerImage"
        android:text="No they won't" />

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/answerImage"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:src="@drawable/none"
        android:layout_marginStart="4dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

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

I have created a drawable to serve as background for the button. There is a TextView to display the answer text and an ImageView that would serve as an anchor for the swipe. With that layout, here is what we have:

Screenshot 1596991123

Creating a MotionScene

Right now, I’m getting an error on my MotionLayout opening tag, it appears we need to fulfil a requirement.

Monosnap 2020 08 09 16 59 38

Android Studio right here is informing me to add an attribute layoutDescription, which will contain the path to our defined MotionScene. A MotionScene is an XML resource file that contains all of the motion descriptions for the corresponding layout. To keep layout information separate from motion descriptions, each MotionLayout references a separate MotionScene.

Right now, I don’t have a MotionScene, so I’m just going to click the ‘Generate MotionScene file’ button provided by Android Studio. After clicking the button, Android Studio automatically generates a MotionScene file, activity_main_scene(based on the name of my layout file) and link it up with my MotionLayout using the layoutDescription attribute. You can also manually do this by simply creating an Android Resource File with MotionScene as the root element and linking it up with your MotionLayout file. Below is the content of the generated MotionScene:

<?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">

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

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

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

Also, below is the MotionScene linked to the MotionLayout using the layoutDescription attribute:

<androidx.constraintlayout.motion.widget.MotionLayout
    android:layout_width="250dp"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    android:id="@+id/button_layout"
    android:background="@drawable/button_layout_bg"
    app:layoutDescription="@xml/activity_main_scene">
    
    ...

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

Because Android Studio is smart, the generated MotionScene already contains possible presets we need to get started, two ConstraitSet and a Transition. <ConstraitSet> is where we define the various constraints that describe our desired motion. It is how we describe our start and end state of our layout. It is quite easy, you simply describe what state you want your layout to be for start and in what state it should be by the time the transition is completed. <Transition> contains the base definition of the motion. It defines the transition type, duration and so on.

Let’s go ahead and add our desired Transition and ConstaintSet. Here, as we swipe the rounded image to the right-hand side, we want to have the rounded image move to the far right and the text moved to the far left. Then at the completion of the transition, we want to have the text colour change as well as the button layout background. We will change the text colour using Custom attributes and will perform the last part in code using MotionLayout TransitionListener.

<?xml version="1.0" encoding="utf-8"?>
<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">

    <Constraint
        android:id="@+id/answerImage"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_marginEnd="4dp"
        android:layout_marginStart="4dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

    <Constraint
        android:id="@+id/answerText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"/>

</ConstraintSet>

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

    <OnSwipe
        motion:touchAnchorId="@+id/answerImage"
        motion:touchAnchorSide="right"
        motion:dragDirection="dragRight" />
</Transition>

</MotionScene>

Let’s analyze our MotionScene above:

  • ConstraintSet (Start): Here, we have the ConstaintSet for start left empty, as we don’t necessarily have any transformation we want to do at the start of the transition, rather than what has been in the button layout itself. In another situation, you may want to transform your elements at the start of the transition, there you can define them.
  • ConstraintSet (End): Here we simply made use of our knowledge of ConstraintLayout to define end states for the ImageView and TextView. Meaning, we want to constrain the start of the text to the start of the parent and the end of the image to the end of the parent. We also defined a couple of margins for our end state and also specified the width and height of the elements, although the same, you may want to have a different size for the end state.
  • Transition: Here we defined the constraint start and end using the id attached to them. We also defined the duration of the transition to be 500ms. Finally, we defined the transition to occur on swipe of the image, using the available attributes, including the dragDirection (dragRight) we want.

With these descriptions, here is the output:

screencast 2020 08 09 18 22 44 2

Adding Custom Attributes

Like I said earlier, we will like the text colour to change at the end of the transition. This can be done by listening for completion of the transition using the MotionLayout TransitionListener and then changing the text colour. However, we will harness the power of Custom Attributes to make this happen.
Within a <Constraint>, you can use the <CustomAttribute> element to specify a transition for attributes that aren’t simply related to position or View attributes. Meaning, you can specify a change in attributes like text colour, simple and easy. Let’s see it in action:

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

    ...

    <Constraint
        android:id="@+id/answerText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent">

        <CustomAttribute
            motion:attributeName="textColor"
            motion:customColorValue="@color/white"/>
    </Constraint>

</ConstraintSet>

You will notice we’ve added a couple more lines of code to our Constraint for the text. Here we specified an attributeName and a customColorValue. MotionLayout simply handles changing of the colour for you.

Using MotionLayout TransitionListener

MotionLayout has a transition listener which informs you of events such as onTransitionTrigger, onTransitionStarted, onTransitionChange and onTransitionCompleted. With this, we can tell when the transition has completed so as to change the background drawable of the button layout. In our Activity onCreate() function, we simply implement the listener:

button_layout.setTransitionListener(object : MotionLayout.TransitionListener{
    override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {

    }

    override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {

    }

    override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {

    }

    override fun onTransitionCompleted(motionLayout: MotionLayout?, currentState: Int) {
        /* Check if user completely transitioned to the right */
        if (currentState == motionLayout?.endState) {
            button_layout.background = ContextCompat.getDrawable(this@MainActivity,
                R.drawable.button_selected_layout_bg)
            button_layout.isInteractionEnabled = false

            //Save answer, make a network request, etc
        }
    }
})

In the onTransitionCompleted function, we check if the user completely transitioned to the right and then change the button background drawable. We also disable interaction with the layout so as to prevent alteration of the selected answer.

Finally, we simply add the question text ? to our layout. Here is the final output:

swipe to answer

That’s it. Easy Peasy! With MotionLayout, we have been able to create a button with smooth transition.

Feel free to check out the github repo for the full source code.


There is a lot more you can achieve using MotionLayout. Also, Android Studio 4.0 (and above) now has Motion Editor which provides a simple interface for manipulating elements from the MotionLayout library, that way you don’t have to manually edit constraints in XML resource files. To learn more about MotionLayout, take some time to check out the documentation. Goodluck exploring! ?