How to add interactive animations to your app with MotionLayout
A few well-placed animations can make your app feel more dynamic and engaging, whether it’s giving users something to look at while you perform work in the background, subtly highlighting the part of your UI that users need to interact with next, or simply adding a flourish to a screen that might otherwise have felt flat and boring.
In this article, we’ll explore MotionLayout, a new class that makes it easier to add complex, interactive animations to your Android apps. By the end of this tutorial, you’ll have used MotionLayout to create a widget that, when tapped, animates across the screen, rotates, resizes, changes color, and responds to user input events.
What is MotionLayout?
The Android framework already provides several solutions for adding animations to your apps, such as TransitionManager and Animated Vector Drawables. However, these solutions can be complex to work with, and some have restrictions that may prevent you from implementing your animations exactly as you’d envisioned them.
MotionLayout is a new class that’s designed to bridge the gap between layout transitions and complex motion handling. Similar to TransitionManager, MotionLayout lets you describe the transition between two layouts. Unlike TransitionManager, MotionLayout isn’t restricted to layout attributes, so you have more flexibility to create highly-customized, unique animations.
At its core, MotionLayout lets you move a widget from point A to point B, with optional deviations and effects in between. For example, you might use MotionLayout to move an ImageView from the bottom of the screen to the top of the screen while increasing the image’s size by 50 percent. Throughout this tutorial, we’ll explore MotionLayout by applying various animations and effects to a button widget.
MotionLayouts is available as part of ConstraintLayout 2.0, so you can create all of your animations declaratively using easy-to-read XML. Plus, since it’s part of ConstraintLayout, all of your MotionLayout code will be backwards compatible to API level 14!
Getting started: ConstaintLayout 2.0
Start by creating a new project. You can use any settings, but when prompted, opt to “Include Kotlin support.”
MotionLayout was introduced in ConstraintLayout 2.0 alpha1, so your project will need access to version 2.0 alpha1 or higher. Open your build.gradle file, and add the following:
implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha2'
How do I create a MotionLayout widget?
Every MotionLayout animation consists of:
- A MotionLayout widget: Unlike other animation solutions such as TransitionManager, MotionLayout only supplies capabilities to its direct children, so you’ll typically use MotionLayout as the root of your layout resource file.
- A MotionScene: You define MotionLayout animations in a separate XML file called a MotionScene. This means that your layout resource file only needs to contain details about your Views, and not any of the animation properties and effects that you want to apply to those Views.
Open your project’s activity_main.xml file, and create a MotionLayout widget, plus the button that we’ll be animating throughout this tutorial.
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.motion.MotionLayout 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" android:id="@+id/motionLayout_container"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="start" android:text="To the right (and back)" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.motion.MotionLayout>
Your UI should look something like this:
Creating a MotionScene and setting some Constraints
The MotionScene file needs to be stored inside an “res/xml” directory. If your project doesn’t already contain this directory, then:
- Control-click the “res” folder.
- Select “New > Android resource directory.”
- Name this directory “xml.”
- Open the “Resource type” dropdown, and select “xml.”
- Click “OK.”
Next, you need to create the XML file where you’ll build your MotionScene:
- Control-click your project’s “res/layout/xml” folder.
- Select “New > XML resource file.”
- Since we’re animating a button, I’m going to name this file “button_MotionScene.”
- Click “OK.”
- Open the “xml/button_motionscene” file, and then add the following MotionScene element:
<?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"> </MotionScene>
Every MotionScene must contain ConstraintSets, which specify the constraints that should be applied to your widget(s) at different points in the animation. A MotionScene typically contains at least two constraints: one representing the animation’s starting point, and one representing the animation’s ending point.
When creating a ConstraintSet, you specify the widget’s desired position and its desired size at this point in the animation, which will override any other properties defined in the Activity’s layout resource file.
Let’s create a pair of ConstraintSets that move the button from the upper-left corner of the screen to the upper-right corner.
<?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/starting_set"> <Constraint android:id="@+id/button" app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </ConstraintSet> <ConstraintSet android:id="@+id/ending_set"> <Constraint android:id="@+id/button" app:layout_constraintRight_toRightOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </ConstraintSet> </MotionScene>
Next, we need to clarify which ConstraintSet represents the animation’s starting point (constraintSetStart) and which ConstraintSet represents its ending point (constraintSetEnd). We place this information inside a Transition, which is an element that allows us to apply various properties and effects to the animation itself. For example, I’m also specifying how long the animation should last.
<?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"> <Transition //The ConstraintSet that you want to use as the animation’s starting point// motion:constraintSetStart="@+id/starting_set" //The ConstraintSet to use as the animation’s ending point// motion:constraintSetEnd="@+id/ending_set" //How long the transition should last// motion:duration="3000"> </Transition> <ConstraintSet android:id="@+id/starting_set"> <Constraint android:id="@+id/button" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintLeft_toLeftOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/ending_set"> <Constraint android:id="@+id/button" motion:layout_constraintRight_toRightOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"> </Constraint> </ConstraintSet> </MotionScene>
Next, we need to make sure our MotionLayout widget is aware of the MotionScene file. Switch back to activity_main.xml, and point MotionLayout in the direction of the “button_MotionScene” file:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.motion.MotionLayout 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" android:id="@+id/motionLayout_container" //The MotionScene that this MotionLayout should use// app:layoutDescription="@xml/button_motionscene"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="start" android:text="Bottom right and back" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.motion.MotionLayout>
Make the button move!
To start this animation, we need to call the transitionToEnd() method. I’m going to call transitionToEnd() when the button is tapped:
import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.view.View import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } //Add the following block// fun start(v: View) { //Animate to the end ConstraintSet// motionLayout_container.transitionToEnd() } }
Install this project on a physical Android smartphone, tablet, or Android Virtual Device (AVD) and give the button a tap. The button widget should respond by moving from one corner of the screen to the other.
At this point we have a problem: once the button has moved to the upper-right corner of the screen, the animation is over and we can’t repeat it unless we exit and relaunch the app. How do we get the button back to its starting position?
Monitoring an animation with transitionToStart()
The easiest way to return a widget to its starting ConstraintSet, is to monitor the animation’s progress and then call transitionToStart() once the animation is complete. You monitor an animation’s progress by attaching a TransitionListener object to the MotionLayout widget.
TransitionListener has two abstract methods:
- onTransitionCompleted(): This method is called when the transition is complete. I’ll be using this method to notify MotionLayout that it should move the button back to its original position.
- onTransitionChange(): This method is called every time the progress of an animation changes. This progress is represented by a floating-point number between zero and one, which I’ll be printing to Android Studio’s Logcat.
Here’s the complete code:
import android.os.Bundle import android.support.constraint.motion.MotionLayout import android.support.v7.app.AppCompatActivity import android.util.Log import android.view.View import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //Add a TransitionListener to the motionLayout_container// motionLayout_container.setTransitionListener( object: MotionLayout.TransitionListener { //Implement the onTransitionChange abstract method// override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) { //Print each floating-point number to Logcat// Log.d("TAG", "Progress:" + progress) } //Implement the onTransitionCompleted method// override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { //If our button is in the ending_set position...// if(currentId == R.id.ending_set) { //...then move it back to the starting position// motionLayout_container.transitionToStart() } } } ) } fun start(v: View) { motionLayout_container.transitionToEnd() } }
As soon as the button reaches the end of the animation, it should automatically reverse through the animation and return to its starting position.
You can also track the animation’s progress as a floating-point number in Android Studio’s Logcat Monitor.
Creating more complex animations: Adding keyframes
Currently, our button moves in a straight line from point A to point B. We can alter the shape of the animation path by defining some intermediate points. If you think of ConstraintSets as MotionLayout’s “resting states,” then keyframes are the points the widget must pass through en route to its next resting state.
MotionLayout supports various keyframes, but we’ll be focusing on:
- KeyPosition: Modifies the path the widget takes during the animation.
- KeyCycle: Adds an oscillation to your animation.
- KeyAttribute: Applies a new attribute value at a specific point during the transition such as changing in color or size.
All keyframes must be placed inside a KeyFrameSet, which in turn must be placed inside a Transition element. Open the “button_motionscene.xml” file and add a KeyFrameSet:
<Transition android:id="@+id/my_transition" app:constraintSetStart="@+id/starting_set" app:constraintSetEnd="@+id/ending_set" app:duration="3000"> <KeyFrameSet android:id="@+id/my_keyframe"> //To do// </KeyFrameSet> </Transition> </MotionScene>
Changing the animation path with KeyPosition
Let’s start by using a KeyPosition keyframe to alter the path our button widget takes through the animation.
A KeyPosition must specify the following:
- motion:target: The ID of the widget that’s affected by the keyframe, which in this instance is the button widget.
- motion:framePosition: The point where the keyframe is applied during the transition, ranging from the animation’s starting point (0) to its ending point (100).
- app:percentX and motion:percentY: Each keyframe’s position is expressed as a pair of X and Y coordinates, although the result of these coordinates will be affected by the project’s motion:keyPositionType.
- motion:keyPositionType: This controls how Android calculates the animation path, and by extension the X and Y coordinates. The possible values are parentRelative (relative to the parent container), deltaRelative (the distance between the widget’s start and end position) and pathRelative (the linear path between the widget’s start and end states).
I’m using KeyPosition to transform the animation’s straight line into a curve:
<Transition motion:constraintSetStart="@+id/starting_set" motion:constraintSetEnd="@+id/ending_set" motion:duration="3000"> <KeyFrameSet> <KeyPosition motion:target="@+id/button" motion:keyPositionType="parentRelative" motion:percentY="1" //The point along the path where this change should occur// motion:framePosition="50"/> </KeyFrameSet> </Transition>
Give the button a tap and it’ll take a new, curved route across the screen.
Making waves: Adding oscillations with Keycycles
You can apply multiple keyframes to the same animation as long as you don’t use multiple keyframes of the same type at the same time. Let’s look at how we can add an oscillation to our animation using KeyCycles.
Similar to KeyPosition, you need to specify the ID of the target widget (app:target) and the point where the keyframe should be applied (app:framePosition). However, KeyCycle also requires a few additional elements:
- android:rotation: The rotation that should be applied to the widget as it moves along the animation path.
- app:waveShape: The shape of the oscillation. You can choose from sin, square, triangle, sawtooth, reverseSawtooth, cos, and bounce.
- app:wavePeriod: The number of wave cycles.
I’m adding a KeyCycle that gives the button a “sin” oscillation of 50 degrees:
<Transition motion:constraintSetStart="@+id/starting_set" motion:constraintSetEnd="@+id/ending_set" motion:duration="3000"> <KeyFrameSet> <KeyPosition motion:target="@+id/button" motion:keyPositionType="parentRelative" motion:percentY="1" motion:framePosition="50"/> <KeyCycle motion:target="@+id/button" motion:framePosition="50" android:rotation="25" //The shape of the wave// motion:waveShape="sin" //The number of wave cycles// motion:wavePeriod="1" /> </KeyFrameSet> </Transition>
Try experimenting with different wave styles, rotations, and wave periods to create different effects.
Scaling up with KeyAttribute
You can specify other widget attribute changes using KeyAttribute.
I’m using KeyAttribute and android:scale to change the size of the button, mid-animation:
<?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"> <Transition motion:constraintSetStart="@+id/starting_set" motion:constraintSetEnd="@+id/ending_set" motion:duration="3000"> <KeyFrameSet> <KeyPosition motion:target="@+id/button" motion:keyPositionType="parentRelative" motion:percentY="1" motion:framePosition="50"/> <KeyCycle motion:target="@+id/button" motion:framePosition="50" android:rotation="25" motion:waveShape="sin" motion:wavePeriod="1" /> //Add the following KeyAttribute block// <KeyAttribute motion:target="@id/button" android:scaleX="2" android:scaleY="2" motion:framePosition="50" /> </KeyFrameSet> </Transition> <ConstraintSet android:id="@+id/starting_set"> <Constraint android:id="@+id/button" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintLeft_toLeftOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/ending_set"> <Constraint android:id="@+id/button" motion:layout_constraintRight_toRightOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"> </Constraint> </ConstraintSet> </MotionScene>
Adding more animation effects: Custom attributes
We’ve already seen how you can use KeyFrames to change a widget’s properties as it moves from one ConstraintSet to the other, but you can further customize your animation using custom attributes.
A CustomAttribute must include the name of the attribute (attributeName) and the value you’re using, which can be any of the following:
- customColorValue
- customColorDrawableValue
- customIntegerValue
- customFloatValue
- customStringValue
- customDimension
- customBoolean
I’m going to use customColorValue to change the button’s background color from cyan to purple as it moves through the animation.
To trigger this color change, you need to add a CustomAttribute to your animation’s start and end ConstraintSet, then use customColorValue to specify the color that the button should be at this point in the transition.
<?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"> <Transition motion:constraintSetStart="@+id/starting_set" motion:constraintSetEnd="@+id/ending_set" motion:duration="3000"> <KeyFrameSet> <KeyPosition motion:target="@+id/button" motion:keyPositionType="parentRelative" motion:percentY="1" motion:framePosition="50"/> <KeyCycle motion:target="@+id/button" motion:framePosition="50" android:rotation="25" motion:waveShape="sin" motion:wavePeriod="1" /> </KeyFrameSet> </Transition> <ConstraintSet android:id="@+id/starting_set"> <Constraint android:id="@+id/button" motion:layout_constraintTop_toTopOf="parent" motion:layout_constraintLeft_toLeftOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"> //Create a custom attribute// <CustomAttribute //Specify that you want to change the background color// motion:attributeName="backgroundColor" //The color the button should be at the start of the animation// motion:customColorValue="#03DAC6" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/ending_set"> <Constraint android:id="@+id/button" motion:layout_constraintRight_toRightOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"> //The color the button should be at the end of the animation// <CustomAttribute motion:attributeName="backgroundColor" motion:customColorValue="#6200EE" /> </Constraint> </ConstraintSet> </MotionScene>
Run this project on your Android device and tap the button to start the animation. The button should gradually change color as it approaches the end ConstraintSet, then shift back to its original color on the return journey.
Making your animations interactive
Throughout this tutorial, we’ve built a complex animation consisting of multiple attribute changes and effects. However, once you tap the button the animation cycles through all of these different stages without any further input from you — wouldn’t it be nice to have more control over the animation?
In this final section we’re going to make the animation interactive, so you can drag the button back and forth along the animation path and through all of the different states, while MotionLayout tracks the velocity of your finger and matches it to the velocity of the animation.
To create this kind of interactive, draggable animation, we need to add an onSwipe element to the Transition block and specify the following:
- motion:touchAnchorId: The ID of the widget that you want to track.
- motion:touchAnchorSide: The side of the widget that should react to onSwipe events. The possible values are right, left, top, and bottom.
- motion:dragDirection: The direction of the motion that you want to track. Choose from dragRight, dragLeft, dragUp, or dragDown.
Here’s the updated code:
</KeyFrameSet> //Add support for touch handling// <OnSwipe motion:touchAnchorId="@+id/button" //The side of the object to move// motion:touchAnchorSide="right" //The side to swipe from// motion:dragDirection="dragRight" /> </Transition>
Run this updated project on your Android device — you should now be able to move the button back and forth along the animation path by dragging your finger across the screen. Note that this feature does seem to be a bit temperamental, so you may need to drag your finger around the screen a bit before you manage to successfully “snag” the button!
You can download this complete project from GitHub.
Wrapping up
In this article, we saw how you can use MotionLayout to add complex, interactive animations to your Android apps and how to customize these animations using a range of attributes.
Do you think MotionLayout is an improvement on Android’s existing animation solutions? Let us know in the comments below!
Learn How To Develop Your Own Android App
Get Certified in Android App Development! No Coding Experience Required.
Android Authority is proud to present the DGiT Academy: the most detailed and comprehensive course covering every aspect of Android app development, run by our own Gary Sims. Whether you are an absolute beginner with zero coding knowledge or a veteran programmer, this course will guide you through the process of building beautiful, functional Android apps and bring you up to speed on the latest features of Android and Android Studio.
The package includes over 6 hours of high quality videos and over 60 different lessons. Reams of in-depth glossaries and resources, highly detailed written tutorials and exclusive access to our private slack group where you can get help directly from Gary and our other elite developers.
AA readers get an additional 60% off today. That's a savings of over $150. Claim your discount now using exclusive promo code: SIXTYOFF. This is your ticket to a lucrative future in Android App Development. What are you wating for?from Android Authority https://ift.tt/2NG1Cwo
Comments
Post a Comment