Blog Infos
Author
Published
Topics
Author
Published
Posted By: Kunal Chaubal

Animations and transitions are essential ingredients for developing any mobile application. Think about how rarely we come across apps that do not have at least one animated component. It helps in keeping the app as user friendly as possible and becomes a very effective marketing tool.

At the same time, developing complex animations and transitions could be quite challenging, and not every developer looks forward to working on such tasks. While the user experience is highly increased with animations, we can’t say the same about development experience.

To address these issues, MotionLayout was introduced as a subclass of ConstraintLayout in v2.0

Objective

We will not be talking about the basics and fundamentals of MotionLayout, but rather take a deep dive on how to implement dynamic transitions with MotionLayout.

At the end of this article, we will be developing the following sample application which demonstrates a bottom navigation bar with some animations:

Sample App screenshot

Prerequisite
  • Basic knowledge of MotionLayout (Check this article that explains some basics concepts pretty well)
  • ConstraintLayout
  • Basic Kotlin knowledge

 

Getting Started

Let’s go through this implementation in a step-by-step process:

  1. Adding the dependency
  2. Creating the UI (without animation)
  3. Generating MotionScene for MotionLayout
  4. Setting up Transitions

Here is a link to the sample repository in case you want to take a look at it first


1. Adding the dependency

Let’s start by adding the dependency constraint layout v2.0 in Gradle:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'
2. Creating the UI (without animation)

Let us begin developing the UI with MotionLayout without adding MotionScene to the app:layoutDescription . The XML file would look something like this:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
tools:context=".MainActivity">
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motion_layout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/colorViewBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:src="@drawable/ic_home_inactive"
app:altSrc="@drawable/ic_home_active"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_home"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:overlay="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_home"
android:textColor="@color/colorText"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_home"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:src="@drawable/ic_search_inactive"
app:altSrc="@drawable/ic_search_active"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_home"
app:layout_constraintTop_toTopOf="parent"
app:overlay="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_search"
android:textColor="@color/colorText"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_like"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_search"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:src="@drawable/ic_like_inactive"
app:altSrc="@drawable/ic_like_active"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_like"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_search"
app:layout_constraintTop_toTopOf="parent"
app:overlay="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_like"
android:textColor="@color/colorText"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_profile"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_like"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:src="@drawable/ic_profile_inactive"
app:altSrc="@drawable/ic_profile_active"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_profile"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_like"
app:layout_constraintTop_toTopOf="parent"
app:overlay="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_profile"
android:textColor="@color/colorText"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_profile"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

ImageFilterView is a ConstraintLayout util widget which can be used for crossfading. We provide the two images in android:src and app:altSrc

app:overlay is set to false so that both the images do not overlap each other

We have used constraint chains to keep the views evenly distributed. There are multiple chaining styles available, you can choose the one that suits your application, best. For this sample app, we have used spread chaining style

app:layout_constraintHorizontal_chainStyle="spread"

Notice that we have set all TextView visibility to ‘gone’. This is because, as per our requirement, only the text of the active tab should be visible. Hence, we are trying to achieve a base state where no tabs are selected at the app launch.

Your app UI after Step 2 should look something like this:

App UI representing Base State


3. Creating MotionScene for MotionLayout

Now, before creating a motion scene file, let’s try to understand what kind of constraint sets and transitions we are trying to achieve

Diagram to represent all States

Diagram to represent all States

States 1–4 are achieved when the respective images are clicked.

Since the view represents a bottom navigation bar, these states are notachieved sequentially. It is not necessary that the state transition will occur only in the following way:

Base State → State 1 → State 2 → State 3 → State 4

In reality, there can be multiple permutations while reaching any state. Hence, to achieve this scenario lets create a constraint set for every State that we discussed until now. The ConstraintSet for base state would look like this:

<ConstraintSet android:id="@+id/base">
<Constraint
android:id="@+id/iv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_home"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
<Constraint
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_home"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
<Constraint
android:id="@+id/iv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_like"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_search"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
<Constraint
android:id="@+id/iv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_profile"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_like"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
</ConstraintSet>
view raw base_state.xml hosted with ❤ by GitHub

ConstraintSet for Base State

Job Offers

Job Offers


    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Kotlin Multiplatform Mobile Developer

    Touchlab
    Remote
    • Full Time
    apply now

    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

Here, we make sure that the default constraints are set to copy the base state. CustomAttribute is used to crossfade the image within the two images we provided earlier. app:customFloatValue has a range of 0 to 1

<CustomAttribute
    app:attributeName="crossfade"
    app:customFloatValue="0" />

The ConstraintSet for State 1 would look like this:

<ConstraintSet android:id="@+id/home_expand"
app:deriveConstraintsFrom="@id/base">
<Constraint
android:id="@+id/iv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_home"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="1" />
</Constraint>
<Constraint
android:id="@+id/tv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_home"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
view raw state_home.xml hosted with ❤ by GitHub

ConstraintSet for State 1

Here, we modify the visibility of the TextView and toggle the image by setting app:customFloatValue to 1

Notice that we have derived constraints from our base state by adding this line of code — app:deriveConstraintFrom=”@id/base"

This is the most important piece of code since we need to make sure that we reset all the other states while transitioning to a user-selected state.

All the constraints from ‘base’ ConstraintSet would be implemented and the constraints that we have further defined in this ConstraintSet are the overridden constraints.

ConstraintSet for other states would be similar to State 1. Here is the final motion_scene.xml file:

<?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/base">
<Constraint
android:id="@+id/iv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_home"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
<Constraint
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_home"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
<Constraint
android:id="@+id/iv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_like"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_search"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
<Constraint
android:id="@+id/iv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_profile"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_like"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="0" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/home_expand"
app:deriveConstraintsFrom="@id/base">
<Constraint
android:id="@+id/iv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_home"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="1" />
</Constraint>
<Constraint
android:id="@+id/tv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_home"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/search_expand"
app:deriveConstraintsFrom="@id/base">
<Constraint
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_search"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_home"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="1" />
</Constraint>
<Constraint
android:id="@+id/tv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_like"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_search"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/like_expand"
app:deriveConstraintsFrom="@id/base">
<Constraint
android:id="@+id/iv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_like"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_search"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="1" />
</Constraint>
<Constraint
android:id="@+id/tv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_profile"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_like"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/profile_expand"
app:deriveConstraintsFrom="@id/base">
<Constraint
android:id="@+id/iv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_profile"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/tv_like"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="crossfade"
app:customFloatValue="1" />
</Constraint>
<Constraint
android:id="@+id/tv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/iv_profile"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>

Don’t forget to link motion_scene.xml to your motion layout in the following way:

 

app:layoutDescription="@xml/motion_scene"
4. Setting up Transitions

Now that we have MotionLayout and MotionScene in place, all that’s left to do is to add transitions to the ConstraintSets we implemented in the previous step.

If you are wondering why didn’t we implement transitions in the same MotionScene file, it is because these transitions are not expected to occur in a sequential static flow. As discussed earlier, depending on what the user selects, constraintSetStart and constraintSetEnd are derived.

Let’s consider the user selection is in the following manner:

  • State 1 → State 3
  • State 3 → State 2

Now, for this transition to work, we would need to defineconstraintSetStart and constraintSetEnd while defining the Transition in motion_scene.xml for both the state changes i.e from State 1 to 3 and State 3 to 2. This doesn’t seem possible to achieve for a dynamic transition.

Hence, to achieve the required transition effect, we would need to define the current state in constraintSetStart during runtime and the ConstraintSet of the user-selected image in constraintSetEnd:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Apply default transition on app launch
motion_layout.setTransition(motion_layout.currentState, R.id.home_expand)
motion_layout.transitionToEnd()
setClickListener()
}
private fun setClickListener() {
iv_home.setOnClickListener {
setTransition(motion_layout.currentState, R.id.home_expand)
}
iv_search.setOnClickListener {
setTransition(motion_layout.currentState, R.id.search_expand)
}
iv_like.setOnClickListener {
setTransition(motion_layout.currentState, R.id.like_expand)
}
iv_profile.setOnClickListener {
setTransition(motion_layout.currentState, R.id.profile_expand)
}
}
private fun setTransition(startState: Int, endState: Int) {
motion_layout.setTransition(startState, endState)
motion_layout.setTransitionDuration(200)
motion_layout.transitionToEnd()
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Depending upon the default state that the user would like to show when app launches, we can set the required transition state

 

motion_layout.setTransition(motion_layout.currentState, R.id.home_expand)
motion_layout.transitionToEnd()

Here, motion_layout.currentState gives us the current state during runtime. R.id.home_expand is the ID of the view we want to highlight.

We have implemented click listeners to identify which view was clicked and set the respective transition accordingly.

iv_search.setOnClickListener {
    setTransition(motion_layout.currentState, R.id.search_expand)
}

 

By setting transitions in the activity, it becomes convenient to set transitions during runtime in motion layout.


That’s all for this article, let me know your thoughts on this approach. Since I am still exploring MotionLayout, if you know any better way to achieve this functionality, I would love to discuss it.

You can find the sample app on GitHub:

KunalChaubal/MotionLayoutExample

Contribute to KunalChaubal/MotionLayoutExample development by creating an account on GitHub.

github.com

 

Feel free to Fork it/contribute to it.

Happy Coding!

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Sometimes it happens to have requests from the UX / UI department that deviate…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu