Android Development

Expand/Collapse Recycler View

January 18, 2017

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 determining 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.

Expand/Collapse Recycler View
<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.

Scott Schmitz
Scott Schmitz
Software Developer

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

Cross tab navigation in Jetpack Compose
Android Development

Cross tab navigation in Jetpack Compose

October 4, 2022

Learn how to use Android's Jetpack Compose to navigate from a screen inside of one NavigationBarItem to another.

Read more
Making your Android project modular with convention plugins
Android Development

Making your Android project modular with convention plugins

May 22, 2024

Explore the journey of Gradle and build tools like it, particularly in the context of Android development. You'll learn the necessity of separating code into modules as projects grow and how Gradle convention plugins can streamline this process.

Read more
The 5 Minute Accessibility Strategy
Android Development iOS

The 5 Minute Accessibility Strategy

May 18, 2023

We discuss how you can make a plan in just 5 minutes to provide accessibility in your mobile app.

Read more
View more articles