MotionLayout - Simplest way to create beautiful Animations in Android

Jossy Paul
4 min readJul 9, 2020

--

3 year before Covid-19 (3 BC)

MotionLayout is a layout type that helps you manage motion and widget animation in your app. MotionLayout is a subclass of ConstraintLayout and builds upon its rich layout capabilities.- Says Developer Site

A week ago I wasn’t even aware that there is a MotionLayout in android. Now that I have started learning it, I can’t believe how easy it is to implement these kind of animations without writing a single line of code in your activity. Before anyone gets too excited about animating the curved bottom portion of image, I have to confess that I am doing this by putting curved image on top of the main image. Not sure whether this is the best way to implement it. But it could be the simplest way.

So that out of the way, let’s analyse the animation that we are about to implement using MotionLayout. Let’s start from the top:

  1. Image at the top transforming to a toolbar. ie. we have to decrease its height and gradually change its image to the background of toolbar.
  2. In oder to achieve the effect of curved ImageView we will draw a curve on top of the ImageView using a vector. And we will decrease its size to zero as it moves up so that will get a morphing effect.
  3. Just before the animation ends, we have to gradually increase the elevation of toolbar aka our ‘ImageView’. So that user will get a feeling that the text is scrolled under the toolbar.
  4. Heading(Manali) size should be decreased as animation moves up.
  5. A trigger that initiates this animation. In our case it will be the swipe gesture on the scrollview at the bottom.

Now let’s start implementing this animation. To implement MotionLayout we need two xml file. One is the actual layout. The other one will be a MotionScene which will contain the starting and ending constraints of views in an animation.

We bind the MotionScene to our layout by setting it as the layoutDescription in our activity_main.xml.

<?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/parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layoutDescription="@xml/activity_main_scene"
tools:context=".MainActivity">

<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_top"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@color/colorPrimary"
android:contentDescription="@string/top"
android:scaleType="centerCrop"
android:src="@drawable/mountain"
app:altSrc="@drawable/test_vector"
app:layout_constraintBottom_toTopOf="@id/scroll_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_top">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-light"
android:text="@string/stub"
android:textSize="24sp" />

</androidx.core.widget.NestedScrollView>

<ImageView
android:id="@+id/iv_curve"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:scaleType="fitXY"
android:src="@drawable/curve"
app:layout_constraintBottom_toTopOf="@id/scroll_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />

<TextView
android:id="@+id/tv_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="12dp"
android:fontFamily="sans-serif-condensed-medium"
android:text="@string/place"
android:textColor="@color/white"
android:textSize="50sp"
app:layout_constraintBottom_toTopOf="@id/scroll_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

As you can see here, we have not set the constraints here. That will be set inside a ConstraintSet in the MotionScene.

We are using ImageFilterView which allows us to switch between two images in an animation. The image specified in the altSrc will be the toolbar background. We will use this to transform the mountain image to toolbar.

Now in order to make the imagview with curved bottom we will create a vectordrawable curve.xml that will be set on to iv_curve.

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="8dp"
android:viewportWidth="100.0"
android:viewportHeight="8.0">
<path
android:fillColor="@color/colorPrimary"
android:pathData="L0,8 100,8 100,0 C100,0 50,17 0,0" />
</vector>

This will be our MotionScene which will contain 2 ConstraintSets activity_main_scene.xml. One will be the starting ConstraintSet and other will be the ending ConstraintSet.

<?xml version="1.0" encoding="utf-8"?>
<MotionScene>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/iv_curve".. />
<Constraint
android:id="@id/tv_heading".. />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/iv_curve".. />
<Constraint
android:id="@id/tv_heading".. />
</ConstraintSet>
</MotionScene>

We have to fill up it with the respective constraints for each views and our final activity_main_scene.xml will be like this.

<?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/iv_top">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
</ConstraintSet>

<ConstraintSet
android:id="@+id/end"
app:deriveConstraintsFrom="@id/start">
<Constraint android:id="@id/iv_top">
<Transform android:translationZ="10dp"/>
<Layout
android:layout_height="60dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="1" />
</Constraint>
<Constraint
android:id="@id/iv_curve"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/scroll_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Constraint android:id="@id/tv_heading">
<Transform
android:scaleX=".5"
android:scaleY=".5" />
</Constraint>

</ConstraintSet>

<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<KeyFrameSet>
<KeyAttribute
android:translationZ="0dp"
app:framePosition="99"
app:motionTarget="@id/iv_top" />
</KeyFrameSet>
<OnSwipe app:touchAnchorId="@+id/scroll_view" />
</Transition>
</MotionScene>

In the starting ConstraintSet we will specify height of iv_curve as wrap_content and in the ending ConstraintSet we will specify it as 0dp. This will create morphing effect to the imageview’s bottom.

Inside Constraint of iv_top we use CustomAttribute tag to fade between two images. Initial image will be the mountain and it will be faded to toolbar background colour as animation progresses. We will use the crossfade here.

In the Transition tag we have to specify the gesture with which this animation occurs. So we will use the OnSwipe and specify the touchAnchorId which will be our scroll view.

Now we want the elevation change of toolbar to be starting only towards the end of animation. Let’s imagine if the overall length of this animation is 100 frames, then we want the elevation to be increasing at the 99th frame. For that we’ll use KeyFrameSet. Here we can specify any attribute at a particular framePosition. As we need the elevation at that point to be zero, we will specify that using translationZ. And in ConstraintSet for the end of animation we will specify translationZ as 10dp.

That’s it. We have implemented some nice animations in no time.

References

https://blog.mindorks.com/getting-started-with-motion-layout-android-tutorials

--

--

Jossy Paul
Jossy Paul

No responses yet