• Complex UI/Animations on Android — featuring MotionLayout
  • Originally written by Nikhil Panju
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Hoarfroster
  • Reviewer:

Explore complex multi-step animations using MotionLayout (and Coroutines).

MotionLayout is new to animation, transformations and complex actions and more. In this article, we’ll look at how MotionLayout and Coroutines can help us build multi-step animations.

The previous article took a deep look at all the different animations and widgets that don’t use MotionLayout. I encourage you to read it because:

  1. In this article we will only discuss filter table transformations, not adapters, tabs, and other animations.
  2. You can understand and appreciate the difference between writing these animations with and without MotionLayout.

Before we get started

  • Think the article is too tedious and long? ** Go directly to Github to see the source code. ! The GitHub repository is well documented and contains two types of code: with and without MotionLayout.
  • Download the application on the PlayStore or build the source code to demonstrate the application. Don’t forget to check the use MotionLayout check box in the navigation drawer.

What is a MotionLayout? Quick introduction…

Simply put, a MotionLayout is a ConstraintLayout that lets you easily convert between two constraintSets.


contains all constraints and layout attributes for each view.

The ConstraintSet that specifies the start and end of the Transition between.

Put it all into a

file, and you have a MotionLayout!

As the layout and animation become more complex, the MotionScene becomes more detailed. We’ll look at each of these components separately.

Learn more about MotionLayout

  • #1 Introduction to MotionLayout series by Nicolas Roard.
  • #2 Advanced & Practical MotionLayout talk by James Pearson.
  • #3 The official Android Developer’s Guide on MotionLayout.

animation

With all the animations taken together, the project’s MotionScene file contains 10 ConstraintSets and nine Transitions. The video below demonstrates all ConstraintSets and Transitions. We will examine a total of 4 animations:

  1. Open the filter table: Set1 → Set2 → Set3 → Set4
  2. Close the filter: Set4 → Set3 → Set2 → Set1
  3. Apply filters: Set4 → Set5 → Set6 → Set7
  4. Filter removal: Set7 → Set8 → Set9 → Set10

Note: The background RecyclerView Items animation is not part of the MotionLayout. Later in this article, we’ll see how to choreograph external animations using MotionLayout.

Each animation (GIF) in this article will display ConstraintSet details below it (for example, Set 4, Transitioning.. , Set 5, etc) to make it easier to understand when reading and navigating the source code.


<ConstraintSet />

ConstraintSets are the building blocks required by MotionLayout to perform the animation. You can specify all constraints, layout properties, and so on here.


must contain a

element that has all layout attributes for each view to animate.

Decompose the <Constraint> tag

We can specify all Layout attributes in the

element, but for more complex animations, we should break them down using the


tags.



Breaking down layout elements allows us to later override only the required attributes without overwriting all of them.

App: deriveConstraintsFrom = “…”

DeriveConstraintsFrom is a very useful tag that allows us to inherit attributes from any other

. This way, we no longer need to rewrite all views/constraints/properties, but only those related to the animation to be set.

Combining this with the previous technique of breaking

elements, we were able to build a concise ConstraintSets that contained only the changes we wanted.

In this project, each of the 10 constraintSets inherits the previous Set and only modifies the properties that need to be animated. For example, in the following transformation, turning off icon rotation is done by inheriting all constraints from “Set5” and applying rotation only in “Set6”.

Warning: When overwriting one of the


elements, all the properties in that element are overwritten, so we may have to copy another property of that element.


Flatten your attempts when necessary

MotionLayout can only use its immediate child views without nested views.

For example, in this animation, the filter icon might look like part of a circular FAB (CardView). But they are divided into different views because they each have their own tasks to accomplish in this animation.

In addition, the FAB height is animated from Set1 to Set2; ICONS must be placed at higher Elvation to be seen; We also don’t want ICONS to have shadows. To prevent this, we can use:

Android: outlineProvider = "no"Copy the code

The shadows are created by the view’s outlineProvider. If we set it to None, the view will have no shadows.

Custom attributes

MotionLayout provides most of the basic properties that we might want to animate. But it can’t give us all the animation properties we want. For example, a custom view might need to animate some other properties.


Bridges this gap by allowing usto use any setter in the view. It uses Reflect to call the method and set the value.

<CustomAttribute
        app:attributeName="radius"
        app:customDimension="16dp"/>
Copy the code

Note: We must use Setter names, not XML attribute names. For example, if CardView has the setRadius() method and the corresponding ATTRIBUTE name in the XML is app:cardCornerRadius, the CustomAttribute should refer to the Setter name — radius.

invisiblegone

Left: Invisible → Visible | Left: Gone → ‘visible

Note this difference when setting visibility from Invisible or Gone to Visible.

✓ Gone → Visible will apply transparency and zoom animations.

✓ Invisible → Visible will only be used for transparency animations.

<Transition />

Transition is a connection between two constraintsets, which specify the state between the start and end of each ConstraintSet.

<Transition  
   app:constraintSetStart="@id/set1"  
   app:constraintSetEnd="@id/set2"  
   app:motionInterpolator="linear"  
   app:duration="300" />
Copy the code

We can also use the

and

elements to specify swiping and click-related functions in transitions, but we won’t discuss them in this article because they are not among the 10 animations discussed in this article.

Interpolator

We can specify Interpolator for transitions using app:motionInterpolator. The attribute values for this attribute can be Linear, easeIn, easeOut, and easeInOut. But there are a few of them. I mean, If we were to compare it to [AnticipateInterpolator] (https://developer.android.com/reference/android/view/animation/AnticipateInterpolator), [BounceI These Interpolators android.com/reference/android/view/animation/BounceInterpolator nterpolator] (https://developer.) In comparison…

Cubic-bezier.com/#0, 1, 5, 1

For these scenarios, we can use the Cubic () option. We can define our own Interpolator here using Bezier curves. We can construct our own Interpolator by making our own Bessel curves at Cubic-bezier.com.

We can set Interpolator using the following method:

App: motionInterpolator = "cubic (,1,0.5 0, 1)Copy the code

Keyframe

Sometimes the start and end states are not enough. For more complex animations, we might want to specify the process of element transitions in more detail. Keyframes help us specify checkpoints in transitions that allow us to change any property of the view at any given time.

Defining motion Paths in MotionLayout takes a closer look at key frames and how to use them.

Left: with keyframes | Right: without keyframes

The animation on the left has 9 keyframes, while the animation on the right has no keyframes.

As you can see, their start (Set 4) and end (Set 5) are the same. But by using keyframes, we have more control over what happens to each element at any time during the transition.

Building keyframes

Each
can have one or more
elements that specify all key frames. For this project, we only use
and
elements.

  • motionTargetSpecifies which view is affected by the keyframe.
  • framePositionSpecify the time (0-100) when keyframes are applied during the transition;
  • <KeyPosition />Used to specify width, height, and x,y coordinate changes;
  • <KeyAttribute />Used to specify any other changesIncluding CustomAttributes;

FramePosition has values of 0 and 1

Sometimes we want to change properties at the beginning of the animation. In normal animation, you can use animator.doonstart {… } or something like that. Let’s try to achieve the same effect using keyframes.

Left: framePosition = 1 | Right: framePosition = 0

In this particular animation, when the user clicks the filter button, the animation starts by changing the FAB (CardView) to a circle and folding it by size.

The problem here is that when framePosition = 0 is used to change the value at the start of the animation, MotionLayout does not record it.

Therefore, if you want to specify a keyframe at the start of any transition, use framePosition = 1 instead.

<KeyAttribute  
   app:motionTarget="@id/fab"  
   app:framePosition="1">  
   <CustomAttribute  
      app:attributeName="radius"  
      app:customDimension="600dp" />  
</KeyAttribute>
Copy the code

Use custom views when necessary

The availability of CustomAttributes allows flexible layout with custom views.

For example, a lot of the transitions in this animation involve FAB (CardView) growing or shrinking in a circular shape. The problem with this is that in order to keep the CardView round, the cornerRadius has to be less than or equal to size/2. Usually we can easily implement this Transition using something like ValueAnimator because we know all the values all the time.

But MotionLayout hides all of our calculations. So to do this, we must introduce a new view:

CircleCardView handles this by limiting the radius to a maximum size/2. Now, when MotionLayout calls the setter (remember CustomAttributes?), We won’t have any more problems.

Choreograph multi-step animations

Currently, MotionLayout does not have an API that allows controlled multi-step transitions. We can use autoTransition, but it’s very limited (we’ll cover that later). In pseudocode, we will do this:

// Transition from set1 -> set2 -> set3 -> set4motionLayout.setTransition(set1, set2) motionLayout.transitionToEnd() motionLayout.doOnEnd { motionLayout.setTransition(set2, set3) motionLayout.transitionToEnd() motionLayout.doOnEnd { motionLayout.setTransition(set3, set4) motionLayout.transitionToEnd() motionLayout.doOnEnd { ... }}}Copy the code

This quickly gets ugly and turns into a terrible callback hell. Coroutines, on the other hand, help us convert asynchronous callback code into linear code.

MotionLayout.awaitTransitionComplete()

Chris Banes’ article on Suspending over Views is a must-read on how to implement coroutines in view-related code.

Pause View — Example, a working example from the Tivi application

He introduces us to awaitTransitionComplete(), which is a suspend function that hides all listeners and lets us use coroutines to easily wait for Transition to complete:

Note: The awaitTransitionComplete() extension method uses a modified MotionLayout that allows multiple listeners to be set instead of just one — (feature request).

Automatic conversion

AutoTransition is the easiest way to implement multi-step transitions without using coroutines. Suppose we want the animation to implement Set7 → Set8 → Set9 → Set10 to implement unfiltered animation.

Now, if we perform motionLayout. TransitionToState (set8), motionLayout to transition from Set7 set8. When it reaches Set8, it automatically converts to Set9. The same is true for Set10.

AutoTransition will perform the transition automatically when MotionLayout reaches the ConstraintSet specified in constraintSetStart.

AutoTransition isn’t perfect

If we watch the animation again, we can notice that the Adapter element in the background is playing the animation. To do these animations in parallel with the MotionLayout transformation, we will have to use coroutines. You can’t synchronize time correctly using autoTransition alone.

private fun unFilterAdapterItems(a): Unit = lifecycleScope.launch {
  
  // 1) Set7 -> Set8
  motionLayout.transitionToState(R.id.set8)
  startScaleDownAnimator(true) // Simulataneous
  motionLayout.awaitTransitionComplete(R.id.set8)
  
  // 2) Set8 -> Set9
  (context as MainActivity).isAdapterFiltered = false // Simulataneous
  motionLayout.awaitTransitionComplete(R.id.set9)
  
  // 3) Set9 -> Set10
  startScaleDownAnimator(false) // Simulataneous
  motionLayout.awaitTransitionComplete(R.id.set10)
}
Copy the code

Rows marked //Simultaneous occur in parallel with the transformation that is taking place.

Since autoTransition doesn’t wait to jump from one transition to the next, awaitTransitionComplete() only lets us know when the transition is complete. It does not actually wait at the end of the transformation. This is why we started with transitionToState() only once.

There are many steps forward and backward

AutoTransition combined with coroutine helps us to control multi-step transitions.

But what if we want to animate backwards (Set4 → Set1) as we reverse each transition?

We can reverse specific transitions, such as Set4 → Set3, by using transitionToStart(). But if we use autoTransition, then it will animate to Set3, and then it will automatically return to Set4 because of autoTransition.

intro

The code that opens the filter Sheet is slightly different from what we saw in the previous section, because we didn’t use autoTransition.

/** Animation sequence: Set1 -> Set2 -> Set3 -> Set4 */
private fun openSheet(a): Unit = lifecycleScope.launch {
  
  // Set the transition.
  // This is necessary because the unfilter animation ends with set10,
  // We need to reset the Sheet the next time we open it
  motionLayout.setTransition(R.id.set1, R.id.set2)
  
  // 1) Set1 -> Set2
  motionLayout.transitionToState(R.id.set2)
  startScaleDownAnimator(true) // Simultaneous
  motionLayout.awaitTransitionComplete(R.id.set2)
  
  // 2) Set2 -> Set3
  motionLayout.transitionToState(R.id.set3)
  motionLayout.awaitTransitionComplete(R.id.set3)
  
  // 3) Set3 -> Set4
  motionLayout.transitionToState(R.id.set4)
  motionLayout.awaitTransitionComplete(R.id.set4)
}
Copy the code
  • We must use it after every waittransitionToState(). This was not necessary before becauseautoTransitionWould run all of this directly without waiting, but for now, we have to do this manually.
  • Note that we are not calling after every waitsetTransition()This is becauseMotionLayoutWill be based on the current ConstraintSet andtransitionToState()ConstraintSet is mentioned in to determine the transition to use.

Close the Sheet animation (reverse)

/** Animation sequence: Set4 -> Set3 -> Set2 -> Set1 */
private fun closeSheet(a): Unit = lifecycleScope.launch {
  
  // We don't need to call setTransition() here, because the current transition is Set3 -> Set4.
   // transitionToStart() will automatically start from:
  // 1) Set4 -> Set3
  motionLayout.transitionToStart()
  motionLayout.awaitTransitionComplete(R.id.set3)
    
  // 2) Set3 -> Set2
  motionLayout.setTransition(R.id.set2, R.id.set3)
  motionLayout.progress = 1f
  motionLayout.transitionToStart()
  motionLayout.awaitTransitionComplete(R.id.set2)
  
  // 3) Set2 -> Set1
  motionLayout.setTransition(R.id.set1, R.id.set2)
  motionLayout.progress = 1f
  motionLayout.transitionToStart()
  startScaleDownAnimator(false) // Simultaneous
  motionLayout.awaitTransitionComplete(R.id.set1)
}
Copy the code

Since all
elements are forward-based, we had to add a few lines to make them work backwards. Its essence is:

// Set transitions to reverse (MotionLayout can only detect forward transitions).
motionLayout.setTransition(startSet, endSet)
// This sets the progress of the transition to the end
motionLayout.progress = 1f
// Reverse the transition from end to beginning
motionLayout.transitionToStart()
// Wait for the transition to begin
motionLayout.awaitTransitionComplete(startSet)
// Repeat the steps for each transition...
Copy the code

✔️ This now allows us to step through multiple transformations in reverse, while maintaining the ability to perform other operations in parallel.

Conclusion – What’s the difference with and without MotionLayout?

Combined with coroutines, MotionLayout makes it easy to implement very complex animations with very little code, while maintaining a flat view hierarchy!

In my last article, I looked at how all of this can be done without using MotionLayout, and the amount of code required to complete the previous chapter ** is much larger. We were also forced to use a lot of math to make the animation work, or to allow us to construct complex view hierarchies, etc.

MotionLayout takes away all the nonsense and leaves us with only what is necessary. With coroutines and upcoming IDE editors, the possibilities for MotionLayout could be endless!

Hope you enjoyed this article 😃! If you’re interested, you can check out some of the source code for this article!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.