MotionLayout & MotionScene: Kotlin XML Guide to Android Animations

MotionLayout is a powerful layout type in Android that allows you to create complex animations and transitions in a declarative way. It’s part of Android’s ConstraintLayout library and builds upon its flexibility, adding the capability to animate between different constraint sets. One of the core components of MotionLayout is the MotionScene, defined in an XML file, which dictates the animations and transitions.

What is a MotionScene?

A MotionScene is an XML resource file that defines the animation sequences for a MotionLayout. It contains details about how different constraints (start and end states) animate, triggers for transitions, and custom attributes for animation.

Why Use a MotionScene?

  • Declarative Animations: Defines animations in XML rather than programmatically, making it easier to manage complex transitions.
  • Lifecycle-Aware: Integrates well with Android’s lifecycle.
  • Constraint-Based Animations: Leverages ConstraintLayout to easily animate changes in constraints.
  • Reduced Code Complexity: Simplifies animation logic compared to traditional animation methods.

How to Create a MotionScene for MotionLayout in Kotlin/XML

To implement MotionLayout animations using a MotionScene, follow these steps:

Step 1: Add the Dependency

Ensure that you have ConstraintLayout and AppCompat dependencies in your build.gradle file:

dependencies {
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    implementation("androidx.appcompat:appcompat:1.6.1")
}

Note that MotionLayout functionalities are bundled within the ConstraintLayout library, so having it is sufficient.

Step 2: Create a MotionLayout Layout File

First, create a MotionLayout XML layout file. This is where you’ll declare your views and specify which MotionScene to use.

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

    <View
        android:id="@+id/viewA"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/design_default_color_primary"
        android:contentDescription="View A"/>

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

Here, app:layoutDescription="@xml/scene_01" links this layout file to the MotionScene.

Step 3: Create a MotionScene XML File

Next, create the MotionScene XML file (e.g., res/xml/scene_01.xml). This file will define the animation states and transitions.

<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/viewA">
            <PropertySet android:alpha="0.5" />
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@id/end">
        <Constraint android:id="@id/viewA">
            <PropertySet android:alpha="1.0" />
            <Layout
                android:layout_width="128dp"
                android:layout_height="128dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                android:layout_marginEnd="8dp"
                android:layout_marginBottom="8dp"/>
        </Constraint>
    </ConstraintSet>

    <Transition
        android:id="@+id/transition"
        app:constraintSetStart="@id/start"
        app:constraintSetEnd="@id/end"
        app:duration="1000">
       <OnSwipe
           app:dragDirection="dragRight"
           app:touchAnchorId="@id/viewA"
           app:touchAnchorSide="right" />
    </Transition>
</MotionScene>

In this file:

  • ConstraintSet with android:id="@id/start" defines the starting state.
  • ConstraintSet with android:id="@id/end" defines the ending state.
  • Transition links the start and end states, specifying animation details such as duration and touch-based triggers. OnSwipe defines a swipe gesture as a trigger to start this animation

Step 4: Link the MotionScene in Kotlin

In your Activity or Fragment, reference the MotionLayout and trigger the transition.


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintSet

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val motionLayout: MotionLayout = findViewById(R.id.motionLayout)

        // Optionally, programmatically trigger transitions
        // motionLayout.transitionToEnd() // or transitionToStart(), transitionToState()
    }
}

The MotionLayout in your layout will automatically apply the animations specified in scene_01.xml when the layout is inflated. If you need to control the animation programmatically (like in response to a button click), you can use methods like transitionToEnd() or transitionToStart().

Step 5: Adding Custom Attributes

Custom attributes let you animate properties that are not directly supported by default in MotionLayout. You can define custom attributes inside the Constraint element.

For example, animating a custom view property named `customProgress`:


<ConstraintSet android:id="@id/start">
    <Constraint android:id="@id/viewA">
        <CustomAttribute
            app:attributeName="customProgress"
            app:customFloatValue="0.0"/>
        <Layout
            android:layout_width="64dp"
            android:layout_height="64dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"/>
    </Constraint>
</ConstraintSet>

<ConstraintSet android:id="@id/end">
    <Constraint android:id="@id/viewA">
        <CustomAttribute
            app:attributeName="customProgress"
            app:customFloatValue="1.0"/>
        <Layout
            android:layout_width="128dp"
            android:layout_height="128dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"/>
    </Constraint>
</ConstraintSet>

In your custom view, you’ll need to handle this attribute using a setter method. For example, in Kotlin:


class CustomView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    var customProgress: Float = 0f
        set(value) {
            field = value
            invalidate() // Redraw the view when the property changes
        }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Use customProgress to draw based on the animation state
        val paint = Paint().apply {
            color = Color.BLUE
        }
        canvas.drawRect(0f, 0f, width * customProgress, height.toFloat(), paint)
    }
}

Complete Example XML Layout:

<?xml version="1.0" encoding="utf-8"?>
<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/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene_01"
    tools:context=".MainActivity">

    <CustomView
        android:id="@+id/viewA"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/design_default_color_primary"
        android:contentDescription="View A"/>

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

Conclusion

Using MotionScene in Kotlin/XML Android development enables the creation of sophisticated animations with relative ease. By declaring animation states and transitions in XML, you reduce code complexity and leverage the declarative approach to define interactive and dynamic user interfaces. The MotionLayout and MotionScene combo enhances your Android applications with visually rich experiences that were traditionally cumbersome to implement.