RecyclerView item animation & self-contained MotionScene

This is going to be the result after we finish with the RecyclerViewitem animations and the self-contained MotionScenes.

But for now let's focus in putting everything into place. By the end of this step you'll have created the screen below. Simple as it is right now, it's the base for us to work with.

In a RecyclerView we can customize the way it appears as a ViewGroup (enter animation), or animate the differences in its children (add/ remove item). Together we're going to see the second one.

As mentioned in the beginning of this workshop, there are 2 ways you can specify an animation; either from the code or from the XML and the RecyclerView animations are not any different than this. In this workshop we're going to create them through XML.

Let's start by adding some logic to our DetailActivity to prepare for adding our new animations.

ic_remove.xml
<vector android:height="24dp"
    android:tint="#333333"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="24dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="#FF000000"
        android:pathData="M19,13H5v-2h14v2z" />
</vector>
ic_add.xml
<vector android:height="24dp"
    android:tint="#333333"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="24dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="#FF000000"
        android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
button_background.xml
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/colorPrimaryDark">
    <item android:id="@android:id/mask">
        <shape android:shape="oval">
            <solid android:color="@color/colorPrimaryDark" />
            <corners android:radius="40dp" />
        </shape>
    </item>
</ripple>
activity_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    tools:context=".DetailActivity" ...>

    <!-- CircleImageView avatar --> 
    <!-- TextView name --> 

    <ImageButton
        android:id="@+id/bt_increase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:background="?selectableItemBackgroundBorderless"
        android:foreground="@drawable/button_background"
        android:padding="28dp"
        android:src="@drawable/ic_add"
        app:layout_constraintBottom_toTopOf="@+id/guideline_horizontalHalf"
        app:layout_constraintEnd_toStartOf="@+id/guideline_vertical25"
        app:layout_constraintStart_toStartOf="@+id/guideline_vertical25"
        app:layout_constraintTop_toTopOf="@+id/guideline_horizontalHalf"
        tools:ignore="ContentDescription" />

    <TextView
        android:id="@+id/tv_counter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/guideline_horizontalHalf"
        app:layout_constraintEnd_toStartOf="@+id/guideline_verticalHalf"
        app:layout_constraintStart_toStartOf="@+id/guideline_verticalHalf"
        app:layout_constraintTop_toTopOf="@+id/guideline_horizontalHalf" />

    <ImageButton
        android:id="@+id/bt_decrease"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:background="?selectableItemBackgroundBorderless"
        android:foreground="@drawable/button_background"
        android:padding="28dp"
        android:src="@drawable/ic_remove"
        app:layout_constraintBottom_toTopOf="@+id/guideline_horizontalHalf"
        app:layout_constraintEnd_toStartOf="@+id/guideline_vertical75"
        app:layout_constraintStart_toStartOf="@+id/guideline_vertical75"
        app:layout_constraintTop_toTopOf="@+id/guideline_horizontalHalf"
        tools:ignore="ContentDescription" />

    <!-- View second background --> 
    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_added_items"
        android:layout_width="282dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:orientation="horizontal"
        app:reverseLayout="true"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="@+id/fab"
        app:layout_constraintEnd_toStartOf="@+id/fab"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/fab"
        tools:listitem="@layout/li_detail" />
    
    <!-- FAB -->

    <android.support.constraint.Guideline
        android:id="@+id/guideline_verticalHalf"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />

    <android.support.constraint.Guideline
        android:id="@+id/guideline_horizontalHalf"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.5" />

    <android.support.constraint.Guideline
        android:id="@+id/guideline_vertical25"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.25" />

    <android.support.constraint.Guideline
        android:id="@+id/guideline_vertical75"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.75" />

    <android.support.constraint.Guideline
        android:id="@+id/guideline_horizontal25"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.25" />

</android.support.constraint.ConstraintLayout>
DetailActivity.kt
class DetailActivity : AppCompatActivity() {

    private var counter = 0
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        tv_counter.text = counter.toString()
        fab.setImageDrawable(animDrawable)
        fab.setOnClickListener {
            if (!isEditMode) {
                ...
                bt_decrease.visibility = View.VISIBLE
                bt_increase.visibility = View.VISIBLE
            } else {
                ...
                bt_decrease.visibility = View.GONE
                bt_increase.visibility = View.GONE
            }
            isEditMode= !isEditMode
        }

        bt_increase.setOnClickListener { tv_counter.text = "${++counter}" }
        bt_decrease.setOnClickListener {
            tv_counter.text = when {
                counter > 0 -> "${--counter}"
                else -> "$counter"
            }
        }
    }
    ...
li_detail.xml
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/added_item_container"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/iv_added_item"
        android:layout_width="42dp"
        android:layout_height="42dp"
        android:src="@drawable/avatar_1"
        tools:ignore="ContentDescription" />

</android.support.constraint.motion.MotionLayout>

We've put almost everything into place!

Right now, it doesn't do much since we haven't added the adapter yet but let's rewind what we've done so far.

  • We added 2 buttons in our Detail Activity layout; one for adding items and one for removing

  • We also added a counter so that we know how many items we've added and removed

  • We added a RecyclerView to our Detail Activity layout

  • And finally we created the layout for our list item of that RecyclerView. You may have noticed that in contrast with our previous motion layouts, in this one we haven't specified any constraints. The reason for this is because we are going to add our view constraints from our MotionScene XML file. We call this MotionScene "self-contained" since all the ConstraintSets are maintained in a single file.

The upcoming MotionEditor in Android Studio will likely to only support self-contained MotionScene files.

The upcoming MotionLayout editor in Android Studio. Image from "Introduction to MotionLayout - Google Developers"

Last updated