KeyFrameSet in MotionLayout: Complex Android Animations in Kotlin XML

In modern Android development, creating smooth and visually appealing animations can significantly enhance the user experience. MotionLayout, a powerful layout type introduced in ConstraintLayout 2.0, allows developers to choreograph complex animations and transitions efficiently. One of the key features that enhances MotionLayout is the KeyFrameSet, which allows for defining intricate animation paths. This blog post dives into using KeyFrameSet to create complex animation paths within MotionLayout in Kotlin-based Android XML development.

Understanding MotionLayout and KeyFrameSet

MotionLayout is a subclass of ConstraintLayout that bridges the gap between layout transitions and complex view animations. It allows you to define animations in a declarative way using XML. A KeyFrameSet is a container for KeyFrame elements that define specific values for a view’s attributes at given points during the animation.

What is MotionLayout?

  • Declarative Animation: Defines animations in XML, making them easier to manage.
  • Transition-Based: Uses transitions to animate between different constraint sets.
  • Part of ConstraintLayout: Leverages the ConstraintLayout’s flexibility for positioning and sizing.

What is KeyFrameSet?

  • Control Animation Path: Specifies attribute values at specific keyframes.
  • Variety of KeyFrames: Supports KeyAttribute, KeyPosition, KeyCycle, and more.
  • Complex Animations: Allows for the creation of intricate and dynamic animations.

Setting Up MotionLayout

Step 1: Add Dependencies

Ensure that your project has the necessary dependencies by adding the following to your build.gradle file:

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

Make sure to sync the Gradle file after adding the dependency.

Step 2: Create a MotionLayout XML File

Convert your existing layout XML file to a MotionLayout or create a new one. For example, motion_layout_example.xml:

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

    <View
        android:id="@+id/animatedView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="@color/colorPrimary"
        android:contentDescription="@string/animated_view" />

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

Step 3: Define the MotionScene XML File

Create a MotionScene XML file (e.g., scene_example.xml) to define the start and end states, transitions, and keyframes. This is where you will use KeyFrameSet:

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

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="2000">
        <KeyFrameSet>
            <KeyAttribute
                motion:motionTarget="@id/animatedView"
                motion:framePosition="25">
                <CustomAttribute
                    motion:attributeName="backgroundColor"
                    motion:customColorValue="#FF0000" />
            </KeyAttribute>
            <KeyPosition
                motion:motionTarget="@id/animatedView"
                motion:framePosition="50"
                motion:keyPositionType="parentRelative"
                motion:percentX="0.7"
                motion:percentY="0.3" />
            <KeyCycle
                motion:motionTarget="@id/animatedView"
                motion:framePosition="75"
                motion:wavePeriod="1"
                motion:waveShape="sin"
                motion:attributeName="translationX"
                android:value="20dp" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/animatedView"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginStart="16dp"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/animatedView"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginEnd="16dp"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent" />
    </ConstraintSet>
</MotionScene>

KeyFrame Types in Detail

Let’s explore the different types of KeyFrame elements you can use within a KeyFrameSet:

1. KeyAttribute

KeyAttribute allows you to change the attributes of a view at specific points in the animation.

<KeyAttribute
    motion:motionTarget="@id/animatedView"
    motion:framePosition="25">
    <CustomAttribute
        motion:attributeName="backgroundColor"
        motion:customColorValue="#FF0000" />
</KeyAttribute>
  • motion:motionTarget: Specifies the ID of the view to be animated.
  • motion:framePosition: Indicates the position in the animation (0-100).
  • CustomAttribute: Changes specific attributes like backgroundColor.

2. KeyPosition

KeyPosition is used to move a view to a specific position at a certain point in the animation.

<KeyPosition
    motion:motionTarget="@id/animatedView"
    motion:framePosition="50"
    motion:keyPositionType="parentRelative"
    motion:percentX="0.7"
    motion:percentY="0.3" />
  • motion:keyPositionType: Determines how the position is calculated (e.g., parentRelative).
  • motion:percentX: The X coordinate as a percentage of the parent’s width.
  • motion:percentY: The Y coordinate as a percentage of the parent’s height.

3. KeyCycle

KeyCycle lets you create repeating animations or oscillations for specific attributes.

<KeyCycle
    motion:motionTarget="@id/animatedView"
    motion:framePosition="75"
    motion:wavePeriod="1"
    motion:waveShape="sin"
    motion:attributeName="translationX"
    android:value="20dp" />
  • motion:wavePeriod: The number of cycles within the animation.
  • motion:waveShape: The shape of the oscillation (e.g., sin, square).
  • motion:attributeName: The attribute to oscillate (e.g., translationX).

Implementing Animations in Kotlin

To trigger the animation, you’ll need to reference the MotionLayout in your Kotlin code and transition between the start and end states.

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

class MainActivity : AppCompatActivity() {

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

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

        animateButton.setOnClickListener {
            motionLayout.transitionToEnd()
        }

        motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
            override fun onTransitionStarted(motionLayout: MotionLayout, startId: Int, endId: Int) {
                // Transition started
            }

            override fun onTransitionChange(motionLayout: MotionLayout, startId: Int, endId: Int, progress: Float) {
                // Transition is changing
            }

            override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
                // Transition completed
            }

            override fun onTransitionTrigger(motionLayout: MotionLayout, triggerId: Int, positive: Boolean, progress: Float) {
                // Transition triggered
            }
        })
    }
}

Advanced Examples

Example 1: Animating Multiple Properties

Animating multiple properties such as translation, rotation, and scale together.

<KeyFrameSet>
    <KeyAttribute
        motion:motionTarget="@id/animatedView"
        motion:framePosition="40">
        <CustomAttribute
            motion:attributeName="backgroundColor"
            motion:customColorValue="#00FF00" />
        <CustomAttribute
            motion:attributeName="rotation"
            motion:customFloatValue="180" />
        <CustomAttribute
            motion:attributeName="scaleX"
            motion:customFloatValue="1.2" />
        <CustomAttribute
            motion:attributeName="scaleY"
            motion:customFloatValue="1.2" />
    </KeyAttribute>
</KeyFrameSet>

Example 2: Creating a Complex Path with KeyPosition

Using multiple KeyPosition elements to define a complex path.

<KeyFrameSet>
    <KeyPosition
        motion:motionTarget="@id/animatedView"
        motion:framePosition="25"
        motion:keyPositionType="parentRelative"
        motion:percentX="0.3"
        motion:percentY="0.2" />
    <KeyPosition
        motion:motionTarget="@id/animatedView"
        motion:framePosition="50"
        motion:keyPositionType="parentRelative"
        motion:percentX="0.7"
        motion:percentY="0.3" />
    <KeyPosition
        motion:motionTarget="@id/animatedView"
        motion:framePosition="75"
        motion:keyPositionType="parentRelative"
        motion:percentX="0.2"
        motion:percentY="0.8" />
</KeyFrameSet>

Conclusion

Using KeyFrameSet within MotionLayout offers a powerful and flexible way to create intricate and visually stunning animations in your Android applications. By leveraging KeyAttribute, KeyPosition, and KeyCycle, you can control every aspect of your animation’s path and behavior. This approach enhances user experience, making your apps more engaging and visually appealing.