Expand Collapse RecyclerView Cell (Android 4.0+)

Adding animations to expanding/collapsing cells is a simple way to add some high quality polish to your app. Today, the solution we see more frequently is launching a popup window overtop of the current selected content. However, what if the intended outcome is for users to compare items? In this case, launching a popup window actually impedes the users success. Fortunately, this functionality comes standard in Android 4.4+ in the package android.transition.TransitionManager. However, what about apps supporting the rest of the Android ecosystem? For those supporting Android Level 14 - 18, I recommend the Transitions Everywhere library.

API Level 19

As stated above, Android 4.4 provides the android.Transition package which allows for transitions to take place over a short period of time. This can be accomplished by calling the TransitionManager immediately after modifying the contents of the cell. Note that, by default, the TransitionManager will animate a Scene change using an AutoTransition, which in the case of expanding cells works well. For more specific transitions refer to the Android Developer Docs.

import android.transition.TransitionManager;
...
@Override
public void onBindViewHolder(final ItemViewHolder viewHolder, final int position) {
    viewHolder.setup(data.get(position));

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (shouldExpand) {
                viewHolder.expandedView.setVisibility(View.VISIBLE);
                viewHolder.imageView_toggle.setImageResource(R.drawable.collapse_symbol);
            } else {
                viewHolder.expandedView.setVisibility(View.GONE);
                viewHolder.imageView_toggle.setImageResource(R.drawable.expand_symbol);
            }

            TransitionManager.beginDelayedTransition(recyclerView)
        }
    });
}

API Level 14+ Double Tap Explosion

The Transitions Everywhere library backports this functionality to Android 4.0! Once the library has been imported, the syntax is exactly the same.

import com.transitionseverywhere.TransitionManager;
...
if (shouldExpand) {
    viewHolder.expandedView.setVisibility(View.VISIBLE);
    viewHolder.imageView_toggle.setImageResource(R.drawable.collapse_symbol);
} else {
    viewHolder.expandedView.setVisibility(View.GONE);
    viewHolder.imageView_toggle.setImageResource(R.drawable.expand_symbol);
}

Although this is starting to come together, you may notice some problems when testing. Visually the expansion duration may feel too long or too short for the content being displayed. Tapping too quickly can cause a crash if the cell is trying to expand before successfully collapsing. Both of these issues are fixable and addressed in the following section.

API Level 14+ Functioning Solution

The above solution is prone to crashing when an item is tapped quickly, due to IllegalStateException. Fortunately, the logs are quite helpful in this scenario and point directly to the problem.

java.lang.IllegalStateException: The specified child already has a parent. You must 
call removeView() on the child's parent first.

Essentially, the view has not yet been set to GONE before it is being set back to VISIBLE. In this situation, the desired effect is a ChangeBounds transition. By defining the transition up front, the application does not have to make its best guess at determing the transition for us. Also, this sets us down the path for defining the duration of the transition itself. Play around with the timing until you discover a value that feels right for the app, in the example below 125ms is used.

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

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/title"
        android:text="Title!"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/expandView"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:visibility="gone"
        >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/description"
            android:text="Description!!"
            />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/coolImage"
            />

    </LinearLayout>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/toggle"
        android:src="@drawable/expand_symbol"
        />
    
</LinearLayout>

import com.transitionseverywhere.ChangeBounds;
import com.transitionseverywhere.Transition;
import com.transitionseverywhere.TransitionManager;

...

@Override
public void onBindViewHolder(final ItemViewHolder viewHolder, final int position) {
    viewHolder.setup(data.get(position));

    // Since views are recycled, it must be reset to the collapsed state.
    // Alternatively, the expand/collapse state could be trakced for the
    // position and then set visibility as needed
    viewHolder.expandedView.setVisibility(View.GONE);

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            boolean shouldExpand = viewHolder.expandedView.getVisibility() == View.GONE;

            ChangeBounds transition = new ChangeBounds();
            transition.setDuration(125);

            if (shouldExpand) {
                viewHolder.expandedView.setVisibility(View.VISIBLE);
                viewHolder.imageView_toggle.setImageResource(R.drawable.collapse_symbol);
            } else {
                viewHolder.expandedView.setVisibility(View.GONE);
                viewHolder.imageView_toggle.setImageResource(R.drawable.expand_symbol);
            }

            TransitionManager.beginDelayedTransition(recyclerView, transition);
            viewHolder.itemView.setActivated(shouldExpand);
        }
    });
}

Conclusion

Although expanding and collapsing cells may be falling out of favor with an increasing preference for popups, there are still occasions when they can be quite useful. Specifically when a user will want to compare information between two cells. Think about if you were shopping for a TV and you wanted to select the one with highest refresh rate. With popups, you will have to open and close each option while remembering the values associated with each television. Not only does this feel clunky but it requires the user to remember their previous choices. However, with an expanding cell interface, multiple options may fit on the page at the same time and allow for direct comparison. When expanding cells are a requirement, there is no reason not to style them with smooth animations.