Action!Go 🏄MotionLayout: Settle Here! Settle There…Done!

Homan Huang
6 min readApr 5, 2021

--

In my opinion, this 🐛buggy 🏄MotionLayout is worth trying, not because I have to send tens of bugs back to Google in one day.

😱: What?! Are you sure?
👦🏻: You don’t have to. I often got a response email about it’s redirected to which group. No answer yet, Rome was not built in one day.

Ignored those bugs, the MotionLayout is a super easy tool to make the animated UI with two stops. Here is my today’s plan:

A resizable SAT Scores Board.

👣 1. Linear Motion …… → Menu

There are many motions you can make in MotionLayout. In my project, I want to close the SAT scores display from bar mode to the four bricks mode. It’s a linear motion from small size expanding to the larger size.

🙌 2. Prepare the Components …… → Menu

You cannot create a 🏄MotionLayout directly. There is no insert function in the MotionLayout, yet! That is the first complaint that I sent to Google. At present, the only thing you can do is converted from ➰ConstraintLayout.

So the first priority, you need to prepare the components. Please take a look at the bigger version of the SAT Scores Board. Can you count how many items that you need to prepare in the ➰ConstraintLayout?

Here is the result:

You may count for 12 items by ignoring the ➰ConstraintLayouts. That will be a terrible mistake because the ➰ConstraintLayout is part of the object which is changing its dimension. I will give you the detail about this mistake.

  • The positions of each item are not important. You have to redesign the constraint in the 🏄MotionLayout. You can design as like the image I displayed. You may send a complaint like me to Google, “Why do I need to do duplicate work to design a 🏄MotionLayout?”

🟢 3. Line Up in the First Layer …… → Menu

Move all components from the ➰ConstraintLayout to the first layer like this:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout...>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/row_test_takers"
android:layout_width="150dp"
android:layout_height="100dp"
android:background="@android:drawable/toast_frame"
android:backgroundTint="@color/teal_200"
>
</androidx.constraintlayout.widget.ConstraintLayout>

<ImageView
android:id="@+id/img_test_takers"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/test_taker"
app:tint="@color/purple_500"
/>

<TextView
android:id="@+id/label_test_takers"
style="@style/Widget.AppCompat.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="SAT\nTest Takers"
android:textColor="@color/purple_500"
android:textStyle="bold"
/>

<TextView
android:id="@+id/test_takers_record"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="0"
android:textColor="@color/purple_500"
android:textSize="32sp"
/>
...

This is an example from the first group.

🙋🏼: What! I will lose everything that I just designed.
😬: Can you feel what I felt? I have to duplicate my work in every 🏄MotionLayout.

I move the ImageView and two TextViews from their ➰ConstraintLayout “@+id/row_test_takers” to the same layer. Why? The present version of 🏄MotionLayout can only handle the first layer. Here is the example after you make a conversion without moving the components out of the ➰ConstraintLayout.

Which is fine without any mistakes. However, you’ll lose control of everything in the ➰ConstraintLayout. The second layer will be treated as one static view group.

Can you see the issue? The label under the image will not be hidden, no matter what I did on the left-hand side. They are totally different things. It took me hours to figure out the problem because of the layer 🐛issue.

You need to move everything out of the box, no matter what layout that you are using. Here is the good conversion:

There is a conflict issue between the start ConstraintSet which is a Your_Layout_scene.xml file in the XML folder to the Your_Layout.xml in the layout folder.

Open the <>Code of Your_Layout.xml. You need to remove all the position labels in the Your_Layout.xml.

🙋🏻: Those are my design! Can I keep them?
🤓: Sure, paste them to a Text editor.

🌄 4. Design a ConstraintSet …… → Menu

Layout_scene.xml:

Constraint your Views to start ConstraintSet.

After you constraint everything to Start ConstraintSet, you can re-constraint them like the old way you do. Have fun!

There is a refresh issue with the Your_Layout.xml. So you may need to open + close this file X times to verify you have the right spot.

Your_Layout_scene.xml:

After you are done with the Start, you can copy all of them into the End ConstraintSet.

Your_Layout.xml: Click on the end ConstraintSet.

Now, you can work on the end part: Fix the position and shrink the size of each view object. My final result you have seen at the beginning.

🎎 5. Create Transition …… → Menu

Transition from Start to End

Click the transition path to check the process from start to end.

Click the play button to verify your work.

Reverse Transition

You can add a click function to the transition by

A DOT will be created on the Transition.

And open Your_Layout_sense.xml: you will find your OnClick record.

This is only an example.

Copy and paste the old one to a new transition: Swap the ids.

Close and open Your_Layout.xml to check your result.

You can decide the FIRST appearance of your 🏄MotionLayout by move which Transition to the top.

📥 6. Insert 🏄MotionLayout …… → Menu

How do we use the external layout?

Easy, we can use < include > layout.

For example,

I add a TextView, “@+id/label_sat_title”, to ask the user to tap on the 🏄MotionLayout.

🏇 7. Use 🏄MotionLayout to Control Other Object …… → Menu

This is the advanced function of MotionLayout. You can control other view objects by transition state — MotionLayout.TransitionListener.

Here is the Live Template:

$layout$.setTransitionListener(
object: MotionLayout.TransitionListener {
override fun onTransitionStarted(p0: MotionLayout?,
p1: Int, p2: Int) {
// work on here
}

override fun onTransitionChange(
motionLayout: MotionLayout?,
startId: Int, endId: Int, progress: Float) { }

override fun onTransitionCompleted(
motionLayout: MotionLayout?,
currentId: Int) { }

override fun onTransitionTrigger(
motionLayout: MotionLayout?,
p1: Int, p2: Boolean, p3: Float) { }
}
)

Use this shortcut to add a MotionLayout.TransitionListener to control the title hide and appear.

I add a global variable:

// sat scorebar switch
private var scorebarSwitch = false

MotionLayout.TransitionListener:

// observer
scoresVM.scores.observe(viewLifecycleOwner, {
if (it != null) {
lgd("===== update: $it")
// update UI
val scoresDetail = binding.scoresDetail
scoresDetail.testTakersRecord.text = it.numOfSatTestTakers
scoresDetail.criticalReadingRecord.text = it.satCriticalReadingAvgScore
scoresDetail.mathRecord.text = it.satMathAvgScore
scoresDetail.writingRecord.text = it.satWritingAvgScore

binding.scoresDetail.mlTestTaker.setTransitionListener(
object: MotionLayout.TransitionListener {
@RequiresApi(Build.VERSION_CODES.R)
override fun onTransitionStarted(p0: MotionLayout?,
p1: Int, p2: Int) {
val scoreTitle = binding.labelSatTitle
if (scorebarSwitch)
scoreTitle.visibility = View.VISIBLE
else
scoreTitle.visibility = View.GONE

}

override fun onTransitionChange(
motionLayout: MotionLayout?,
startId: Int, endId: Int, progress: Float
) { }

override fun onTransitionCompleted(
motionLayout: MotionLayout?,
currentId: Int) {
scorebarSwitch = currentId ==
binding.scoresDetail.mlTestTaker.startState

}

override fun onTransitionTrigger(
motionLayout: MotionLayout?,
p1: Int, p2: Boolean, p3: Float) { }
}
)

}
})

That’s it! Run and see the result. When you tap on the 🏄MotionLayout, the labelSatTitle TextView will follow to appear or hide its body.

Have Fun!

Please clap if you like my story! 😅

--

--

Homan Huang

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.