In this tutorial we will look at several options of achieving an expandablerecyclerview. We know that there is a standard ExpandableListView in android sdk. However due to the nature of a recyclerview being more of a framework than a concrete widget, we have to implement our own logic to achieve an ExpandableRecyclerView.

In this article you will learn:

  1. How to create a custom expandable recyclerview – WITHOUT or WITH a library

We use the following languages:

  1. Kotlin
  2. Java

NB/= This article is broken down into several pages. Each page teaches a unique concept. Here are the taught concepts:
Page 1: Implement a custom Expandable Reyclerview without a third party library.
Page 2: Implement an expandable recyclerview using RecyclerView and Expandable layouts.

Concept 1: Expandable RecyclerViews WITHOUT third-party libraries

In the first page or section you will learn how to implement an expandable recyclerview without any third party library.

Example 1: Sectioned Expandable Grid RecyclerView example

In this example you will learn how to create a sectioned expandable recyclerview grid. This is perfect as a template to use if you want to categorize or group items in your recyclerview.

The section headers are expandable/collapsible and can thus reveal and hide the child items. This example is written in Java.

Demo

Here is the demo:

Android Sectioned Expandable Grid recylerview example

Step 1: Add RecyclerView

We are not using any third party library to achieve this. However you need to add recyclerview as well as cardview in your dependencies closure.

Step 2: Create Layouts

We will need these layouts in our project:

  1. Item layout.
  2. Expandable Header layout.
  3. Main Activity layout.

Please feel free touse androidx cardviews and recyclerviews.

(a). layout_item.xml

Add a cardview and textview. Feel free to use androidx cardview.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:elevation="5dp"
    android:layout_margin="@dimen/item_margin"
    android:layout_width="wrap_content"
    android:layout_height="@dimen/item_height">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/text_item"
        android:gravity="center"
        android:layout_gravity="center" />

</android.support.v7.widget.CardView>

(b). layout_header.xml

This layout will represent a single header. Add a textview to render title and also a togglebutton to be used to toggle expandable state.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    app:elevation="5dp"
    android:layout_marginTop="7dp"
    android:layout_height="wrap_content">

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="#26A69A"
       android:orientation="horizontal">

       <TextView
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:paddingTop="@dimen/section_padding"
           android:paddingBottom="@dimen/section_padding"
           android:paddingStart="@dimen/section_text_padding_left"
           android:background="@drawable/theme_color"
           android:textColor="@color/default_text_color"
           android:textSize="@dimen/section_text_size"
           android:id="@+id/text_section"
           android:layout_weight="0.12"/>

       <ToggleButton
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:id="@+id/toggle_button_section"
           android:contentDescription="@string/image_button_content_description"
           android:background="@drawable/selector_section_toggle"
           android:padding="@dimen/section_padding"
           android:textOn=""
           android:textOff=""
           android:layout_weight="0.88"/>

   </LinearLayout>

</android.support.v7.widget.CardView>

(c). activity_main.xml

This is the main activity layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:background="#E0F2F1"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycler_view"/>

</RelativeLayout>

Step 3: Create Model Classes

Start by creating model classes:

(a). Item.java

This is the data object for a single item:

public class Item {

    private final String name;
    private final int id;

    public Item(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

(a). Section.java

This modelclass will represe a single section in our recyclerview:

public class Section {

    private final String name;

    public boolean isExpanded;

    public Section(String name) {
        this.name = name;
        isExpanded = true;
    }

    public String getName() {
        return name;
    }
}

Step 4: Create Event Listeners

Create the following listeners as interfaces:

(a). ItemClickListener.java

This interface will define two event handlers as abstract methods: itemClicked() for both our item and section:

public interface ItemClickListener {
    void itemClicked(Item item);
    void itemClicked(Section section);
}

(b). SectionStateChangeListener.java

These are the state change event handlers for our expandable recycerlview:

public interface SectionStateChangeListener {
    void onSectionStateChanged(Section section, boolean isOpen);
}

Step 5: Create Adapters and Helpers

(a). SectionedExpandableGridAdapter.java

Create our expandable grid adapter:

public class SectionedExpandableGridAdapter extends RecyclerView.Adapter<SectionedExpandableGridAdapter.ViewHolder> {

    //data array
    private ArrayList<Object> mDataArrayList;

    //context
    private final Context mContext;

    //listeners
    private final ItemClickListener mItemClickListener;
    private final SectionStateChangeListener mSectionStateChangeListener;

    //view type
    private static final int VIEW_TYPE_SECTION = R.layout.layout_section;
    private static final int VIEW_TYPE_ITEM = R.layout.layout_item; //TODO : change this

    public SectionedExpandableGridAdapter(Context context, ArrayList<Object> dataArrayList,
                                          final GridLayoutManager gridLayoutManager, ItemClickListener itemClickListener,
                                          SectionStateChangeListener sectionStateChangeListener) {
        mContext = context;
        mItemClickListener = itemClickListener;
        mSectionStateChangeListener = sectionStateChangeListener;
        mDataArrayList = dataArrayList;

        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return isSection(position)?gridLayoutManager.getSpanCount():1;
            }
        });
    }

    private boolean isSection(int position) {
        return mDataArrayList.get(position) instanceof Section;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(mContext).inflate(viewType, parent, false), viewType);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        switch (holder.viewType) {
            case VIEW_TYPE_ITEM :
                final Item item = (Item) mDataArrayList.get(position);
                holder.itemTextView.setText(item.getName());
                holder.view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mItemClickListener.itemClicked(item);
                    }
                });
                break;
            case VIEW_TYPE_SECTION :
                final Section section = (Section) mDataArrayList.get(position);
                holder.sectionTextView.setText(section.getName());
                holder.sectionTextView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mItemClickListener.itemClicked(section);
                    }
                });
                holder.sectionToggleButton.setChecked(section.isExpanded);
                holder.sectionToggleButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                    @Override
                    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                        mSectionStateChangeListener.onSectionStateChanged(section, isChecked);
                    }
                });
                break;
        }
    }

    @Override
    public int getItemCount() {
        return mDataArrayList.size();
    }

    @Override
    public int getItemViewType(int position) {
        if (isSection(position))
            return VIEW_TYPE_SECTION;
        else return VIEW_TYPE_ITEM;
    }

    protected static class ViewHolder extends RecyclerView.ViewHolder {

        //common
        View view;
        int viewType;

        //for section
        TextView sectionTextView;
        ToggleButton sectionToggleButton;

        //for item
        TextView itemTextView;

        public ViewHolder(View view, int viewType) {
            super(view);
            this.viewType = viewType;
            this.view = view;
            if (viewType == VIEW_TYPE_ITEM) {
                itemTextView = (TextView) view.findViewById(R.id.text_item);
            } else {
                sectionTextView = (TextView) view.findViewById(R.id.text_section);
                sectionToggleButton = (ToggleButton) view.findViewById(R.id.toggle_button_section);
            }
        }
    }
}

(b). .java

Create our layout helper which will be used to add data to the recyclerview:

public class SectionedExpandableLayoutHelper implements SectionStateChangeListener {

    //data list
    private LinkedHashMap<Section, ArrayList<Item>> mSectionDataMap = new LinkedHashMap<Section, ArrayList<Item>>();
    private ArrayList<Object> mDataArrayList = new ArrayList<Object>();

    //section map
    //TODO : look for a way to avoid this
    private HashMap<String, Section> mSectionMap = new HashMap<String, Section>();

    //adapter
    private SectionedExpandableGridAdapter mSectionedExpandableGridAdapter;

    //recycler view
    RecyclerView mRecyclerView;

    public SectionedExpandableLayoutHelper(Context context, RecyclerView recyclerView, ItemClickListener itemClickListener,
                                           int gridSpanCount) {

        //setting the recycler view
        GridLayoutManager gridLayoutManager = new GridLayoutManager(context, gridSpanCount);
        recyclerView.setLayoutManager(gridLayoutManager);
        mSectionedExpandableGridAdapter = new SectionedExpandableGridAdapter(context, mDataArrayList,
                gridLayoutManager, itemClickListener, this);
        recyclerView.setAdapter(mSectionedExpandableGridAdapter);

        mRecyclerView = recyclerView;
    }

    public void notifyDataSetChanged() {
        //TODO : handle this condition such that these functions won't be called if the recycler view is on scroll
        generateDataList();
        mSectionedExpandableGridAdapter.notifyDataSetChanged();
    }

    public void addSection(String section, ArrayList<Item> items) {
        Section newSection;
        mSectionMap.put(section, (newSection = new Section(section)));
        mSectionDataMap.put(newSection, items);
    }

    public void addItem(String section, Item item) {
        mSectionDataMap.get(mSectionMap.get(section)).add(item);
    }

    public void removeItem(String section, Item item) {
        mSectionDataMap.get(mSectionMap.get(section)).remove(item);
    }

    public void removeSection(String section) {
        mSectionDataMap.remove(mSectionMap.get(section));
        mSectionMap.remove(section);
    }

    private void generateDataList () {
        mDataArrayList.clear();
        for (Map.Entry<Section, ArrayList<Item>> entry : mSectionDataMap.entrySet()) {
            Section key;
            mDataArrayList.add((key = entry.getKey()));
            if (key.isExpanded)
                mDataArrayList.addAll(entry.getValue());
        }
    }

    @Override
    public void onSectionStateChanged(Section section, boolean isOpen) {
        if (!mRecyclerView.isComputingLayout()) {
            section.isExpanded = isOpen;
            notifyDataSetChanged();
        }
    }
}

Run

Lastly run the project and you will get a result similar to the one shown in the screenshot above.

Reference

Find the full code below.

No. Link
1. Download code
2. Browse code
3. Follow Code Author

Example 2: Kotlin Android ExpandableRecyclerView

This second example is a written in Kotlin. This is also a much simpler one since we do have sections, just expandable items.

Here is the screenshot of what we will create:

 Kotlin Android ExpandableRecyclerView Example

Step 1: Create Project

Start by creating an empty Android Studio project.

Step 2: Dependencies

We will be using butterknife to reference views. This is OPTIONAL.

    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

Step 3: Design Layouts

Design two layouts:

(a).list_item.xml

This will represent a single item in the recyclerview. It encompases both header and content.

<?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"
    android:stateListAnimator="@animator/translate_z"
    android:background="@android:color/white">

    <TextView
        android:id="@+id/headerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:paddingTop="16dp"
        android:paddingBottom="16dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Title"
        tools:text="Header"/>

    <TextView
        android:id="@+id/contentView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:paddingBottom="16dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        tools:text="Content" />

</LinearLayout>

(b).activity_main.xml

In this layout we will have a recyclerview:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.paranoid.mao.expandablerecyclerviewdemo.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/expandableRecycleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

Step 4: Create Model class

We create our data class:

DummyData.kt

data class DummyData(val header: String, val content: String)

Step 5: Create a custom Animator

Extend the DefaultAnimator and override the animateMove() to create a DefaultAnimator:

class Animator(): DefaultItemAnimator() {
    // Invalid recycler view moves items which causes flash when expanding or collapsing
    override fun animateMove(holder: RecyclerView.ViewHolder?, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean {
        return false
    }
}

Step 6: Create an Expandable Adapter

Create an Expandable Adapter class:

ExpandableAdapter.kt

class ExpandableAdapter(private var dataList: List<DummyData>, private val parent: RecyclerView): RecyclerView.Adapter<ExpandableAdapter.ViewHolder>() {

    companion object {
        const val EXPAND_COLLAPSE = "EXPAND_COLLAPSE"
    }

    private var expandedPosition = RecyclerView.NO_POSITION
    private val transition = AutoTransition().apply { duration = 100 }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
        if (payloads?.contains(EXPAND_COLLAPSE) == true) {
            setExpanded(holder, expandedPosition == position)
        } else {
            onBindViewHolder(holder, position)
        }
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.apply {
            bind(dataList[position])
            itemView.setOnClickListener {
                TransitionManager.beginDelayedTransition(parent, transition)
                // collapse currently expanded items
                if (RecyclerView.NO_POSITION != expandedPosition) {
                    notifyItemChanged(expandedPosition, EXPAND_COLLAPSE)
                }

                // expand this item
                if (expandedPosition != adapterPosition) {
                    expandedPosition = adapterPosition
                    notifyItemChanged(adapterPosition, EXPAND_COLLAPSE)
                } else {
                    expandedPosition = RecyclerView.NO_POSITION
                }
            }
            setExpanded(this, expandedPosition == position)
        }
    }

    private fun setExpanded(holder: ViewHolder, isExpanded: Boolean) {
        val visibility = if (isExpanded) View.VISIBLE else View.GONE
        holder.itemView.apply {
            isActivated = isExpanded
            contentView.visibility = visibility
        }
    }

    override fun getItemCount(): Int = dataList.size

    inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        fun bind(data: DummyData) = with(itemView) {
            headerView.text = data.header
            contentView.text = data.content
        }
    }
}

Step 7: Create MainActivity

Finally create your MainActivity as follows:

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val dataList = List(26, { i ->
            DummyData("Header ${'A' + i}", "Content ${i + 1}")
        })

        expandableRecycleView.apply {
            val manager = LinearLayoutManager([email protected])
            layoutManager = manager
            adapter = ExpandableAdapter(dataList, this)
            itemAnimator = Animator()
            addItemDecoration(DividerItemDecoration([email protected], manager.orientation))
        }

    }
}

Run

Copy the code or download it in the link below, build and run.

Reference

Here are the reference links:

Number Link
1. Download Example
2. Follow code author

Concpet 2: ExpandableRecyclerViews with ExpandableLayouts

Proceed to next page to learn RecyclerView + Expandable layouts to create an expandable recyclerview.