The expansion and folding of RecyclerView is a common animation. There are two main ways to achieve 1. By adding and removing elements notifyInsert notifyRemoved, this approach involves elements of add and subtract, animation effect is not very smooth. 2. In the case of adding animations to RecyclerView items, we need to consider the influence of adding animations to one item on other items. MotionLayout is a handy way to do this.

Let’s take a look at the effect

Support smooth expansion folding 2. Support multiple types of item 3. Support only one expansion at the same time

Let’s look at the implementation

Introducing MotionLayout library

Dependencies {implementation 'com. Android. Support. The constraint, the constraint - layout: 2.0.0 - beta 2'}Copy the code

Used in layout files

MotionLayout To use MotionLayout, just declare the following in the layout file:

<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/motionContainer"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    app:layoutDescription="@xml/motion_list_rv_item_scene">
.....
</android.support.constraint.motion.MotionLayout>
Copy the code

Because MotionLayout is a subclass of ConstraintLayout, it’s natural to use it like ConstraintLayout to constrain subviews, but that’s a bit of an overuse. MotionLayout is much more useful than that. Let’s first look at the composition of MotionLayout:

As you can see from the figure above, MotionLayout can be divided into two parts: and. Part of this is simply interpreted as a ConstraintLayout, which is our “animation layer.” MotionLayout gives us the layoutDescription property, and we need to pass it an XML file wrapped around the MotionScene. To achieve animation interaction, we must connect through this “medium.”

MotionScene

What is a MotionScene? StateSet, ConstraintSet and Transition are used to expand and fold the RecyclerView, using ConstarintSet and Transition

First, look at the layout file

<? The 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/motionContainer" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" app:layoutDescription="@xml/motion_list_rv_item_scene"> <LinearLayout android:id="@+id/box_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="86dp"> .... </LinearLayout> </LinearLayout> <View android:layout_width="match_parent" android:layout_height="100dp" android:background="@color/blue_magic" /> </LinearLayout> <View android:id="@+id/view2" android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/box_content" android:background="#eaeaef" /> </androidx.constraintlayout.motion.widget.MotionLayout>Copy the code

The layout file is simple, but you may notice that we don’t add any constraints to the LinearLayout because: We declare ConstraintSet in the MotionScene, which will contain constraint information about the start and end of the LinearLayout’s “motion”.

You can also restrict it in the layout file, but control constraints in MotionScene take precedence over Settings in the layout file. Here we use layoutDescription to set the MotionLayout’s MotionScene to motion_list_rv_ITEM_scene.

Animation files

<? The 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/box_content" android:layout_width="0dp" android:layout_height="86dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </ConstraintSet> <ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/box_content" android:layout_width="0dp" android:layout_height="186dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </ConstraintSet> <Transition app:constraintSetEnd="@id/end" app:constraintSetStart="@+id/start" app:duration="500" app:motionInterpolator="easeInOut"> </Transition> </MotionScene>Copy the code

Firstly, it can be found that we define two constraint information that respectively describe the start and end position of the animation of the item in RecyclerView (only containing a small amount of necessary information, such as width, height, margin and position attribute, etc.). ItemView starts at 86dp and ends at 186dp.

So the question is, how do you make it work? It depends on our elements.

In fact, we all know, animation is a start and end position, and use the objective fact MotionLayout, separate the fore and aft position and animation process, although two point location and distance is fixed, but the Path between them is infinite, can be “flat”, or “winding”.

All we need to do is set the start and end constraintsets for Transition and set the animation time. MotionLayout does the rest automatically.

You can also trigger an animation with the onClick event, bind the id of the target control, and set the click event type with the clickAction property.

There are several types of OnClick

  • 1. Toggle: If the layout is currently in the start state, please switch the animation effect to the end state; Otherwise, switch the animation effect to the start state.
  • 2. TransitionToStart, from the current layout to the motion of the element: : constraintSetStart attribute specifies the layout of the add animation effects.
  • 3. TransitionToEnd, which animates the layout specified from the current layout to the element’s Motion :constraintSetEnd attribute.

Only one item implementation can be expanded at a time

Because we need to expand one item and collapse the other items, so instead of specifying click events in the XML, we go to the Adapter and specify the implementation to expand one item and collapse the other items and we can tell by the progress of the MotionLayout whether we are currently in the start or end state.

The following code has a few main points to note: 1. Expand if it is in the start state, otherwise collapse 2. The partial refresh of the payload folds other ItemViews. 3. It will be reused in RecyclerView rolling, so it is necessary to initialize the state of item, namely progress, when onBindViewHolder, otherwise dislocation phenomenon will occur

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is MotionViewHolder) {
            val motionBox = holder.itemView.findViewById<MotionLayout>(R.id.motionContainer)
            if (expandList[position]){
                motionBox.progress = 1.0 f
            }else{
                motionBox.progress = 0f
            }

            holder.itemView.setOnClickListener {
                expandList.fill(false)
                if (motionBox.progress == 1.0 f) {
                    motionBox.transitionToStart()
                } else if (motionBox.progress == 0.0 f) {
                    motionBox.transitionToEnd()
                    expandList[position] = true
                }
                for (i in 0 until itemCount) {
                    if(i ! = position) { notifyItemChanged(i,"collapse")}}}}}override fun onBindViewHolder(
        holder: RecyclerView.ViewHolder,
        position: Int,
        payloads: MutableList<Any>) {
        if (payloads.isNullOrEmpty()) {
            super.onBindViewHolder(holder, position, payloads)
        } else {
            if (holder is MotionViewHolder) {
                val motionBox = holder.itemView.findViewById<MotionLayout>(R.id.motionContainer)
                motionBox.transitionToStart()
            }
        }
    }
Copy the code

conclusion

Through the above steps, the use of MotionLayout is relatively simple to achieve the RecyclerView item expansion folding effect 1. Support smooth expansion folding 2. Support multiple types of item 3. Support only one expansion at the same time

MotionLayout has many more powerful features, such as associating with AppBarLayout, associating with Lottie, and implementing complex animations. If you are interested, please refer to the reference links below, as well as all the code in this article

All relevant code for this article

MotionLayoutRecyclerView implementation

The resources

MotionLayout: Open a new world of Animation (Part I