Expandable RecyclerView in Android

    I have many posts about RecyclerView -  an entirely updated approach to showing a collection of data. Google developers released this widget as an alternative to the predecessor: ListView. While most of what RecyclerView offers is an improvement over the existing functionality of ListView, there are a few notable features missing from the RecyclerView API. For example, we lost our dear friend OnItemClickListener and our lesser, but still kinda close friend, ChoiceModes. And if all that lost functionality wasn’t depressing enough, we also lost all of the sub-classes and custom implementations of ListView, like ExpandableListView.
    So, if we would like to build an expandable list view by RecyclerView, we must customize it. Today, in this post, I will present a third-party library developed by ThoughtBot, Inc which I think it's the best to this time, which providing us a full-featured expandable list view.
    DEMO VIDEO:

Importing the library

    In order to use it, please add this dependency to your app-level build.gradle:
compile 'com.thoughtbot:expandablerecyclerview:1.0'

Customizing POJO classes

    An expandable list view has 2 view types: group and child view. So, we must have 2 POJOs to declaring them. In this project, I create 2 classes: MobileOS (as a group object) and Phone (stand as child object):
MobileOS.java
package info.devexchanges.expandablerecyclerview.model;

import android.annotation.SuppressLint;
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup;
import java.util.List;

@SuppressLint("ParcelCreator")
public class MobileOS extends ExpandableGroup<Phone> {

    public MobileOS(String title, List<Phone> items) {
        super(title, items);
    }
}
Phone.java
package info.devexchanges.expandablerecyclerview.model;

import android.os.Parcel;
import android.os.Parcelable;

public class Phone implements Parcelable{
    private String name;

    public Phone(Parcel in) {
        name = in.readString();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Phone(String name) {
        this.name = name;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<Phone> CREATOR = new Creator<Phone>() {
        @Override
        public Phone createFromParcel(Parcel in) {
            return new Phone(in);
        }

        @Override
        public Phone[] newArray(int size) {
            return new Phone[size];
        }
    };
}
    As you can see, the child class must implement Parcelable and the group class must have a List of child class, express that a group has many children in expandable list view.

Group and Child ViewHolder

    The next step is creating group and child view holders based on GroupViewHolder and ChildViewHolder. These are both wrappers around regular original RecyclerView.ViewHolder so implement any view inflation and binding methods you may need:
OSViewHolder.java
package info.devexchanges.expandablerecyclerview.viewholder;

import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup;
import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder;

import info.devexchanges.expandablerecyclerview.R;

public class OSViewHolder extends GroupViewHolder {

    private TextView osName;

    public OSViewHolder(View itemView) {
        super(itemView);

        osName = (TextView) itemView.findViewById(R.id.mobile_os);
    }

    @Override
    public void expand() {
        osName.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.down_arrow, 0);
        Log.i("Adapter", "expand");
    }

    @Override
    public void collapse() {
        Log.i("Adapter", "collapse");
        osName.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.up_arrow, 0);
    }

    public void setGroupName(ExpandableGroup group) {
        osName.setText(group.getTitle());
    }
}
    As you see, you can override expand() and collapse() to handling group view expand/collapse event.
PhoneViewHolder.java
package info.devexchanges.expandablerecyclerview.viewholder;

import android.view.View;
import android.widget.TextView;

import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup;
import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder;

import info.devexchanges.expandablerecyclerview.R;
import info.devexchanges.expandablerecyclerview.model.Phone;

public class PhoneViewHolder extends ChildViewHolder {

    private TextView phoneName;

    public PhoneViewHolder(View itemView) {
        super(itemView);

        phoneName = (TextView) itemView.findViewById(R.id.phone_name);
    }

    public void onBind(Phone phone, ExpandableGroup group) {
        phoneName.setText(phone.getName());
        if (group.getTitle().equals("Android")) {
            phoneName.setCompoundDrawablesWithIntrinsicBounds(R.drawable.nexus, 0, 0, 0);
        } else if (group.getTitle().equals("iOS")) {
            phoneName.setCompoundDrawablesWithIntrinsicBounds(R.drawable.iphone, 0, 0, 0);
        } else {
            phoneName.setCompoundDrawablesWithIntrinsicBounds(R.drawable.window_phone, 0, 0, 0);
        }
    }
}
    There are 2 xml files to display group and child views of the expandable list view:
group_view_holder.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/black">

    <TextView
        android:id="@+id/mobile_os"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:drawablePadding="5dp"
        android:drawableRight="@drawable/down_arrow"
        android:gravity="left|center"
        android:padding="8dp"
        android:textColor="#e6e600" />

</RelativeLayout>
child_view_holder.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/phone_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="10dp" />
</RelativeLayout>

Creating RecyclerView adapter class

    By extending from ExpandableRecyclerViewAdapter abstract class, we can customize a RecyclerView adapter. Requirement methods are similar with original RecyclerView.Adapter:
RecyclerAdapter.java
package info.devexchanges.expandablerecyclerview.adapter;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter;
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup;

import java.util.List;

import info.devexchanges.expandablerecyclerview.R;
import info.devexchanges.expandablerecyclerview.model.MobileOS;
import info.devexchanges.expandablerecyclerview.model.Phone;
import info.devexchanges.expandablerecyclerview.viewholder.OSViewHolder;
import info.devexchanges.expandablerecyclerview.viewholder.PhoneViewHolder;

public class RecyclerAdapter extends ExpandableRecyclerViewAdapter<OSViewHolder, PhoneViewHolder> {

    private Activity activity;

    public RecyclerAdapter(Activity activity, List<? extends ExpandableGroup> groups) {
        super(groups);
        this.activity = activity;
    }

    @Override
    public OSViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.group_view_holder, parent, false);

        return new OSViewHolder(view);
    }

    @Override
    public PhoneViewHolder onCreateChildViewHolder(ViewGroup parent, final int viewType) {
        LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.child_view_holder, parent, false);

        return new PhoneViewHolder(view);
    }

    @Override
    public void onBindChildViewHolder(PhoneViewHolder holder, int flatPosition, ExpandableGroup group, int childIndex) {
        final Phone phone = ((MobileOS) group).getItems().get(childIndex);
        holder.onBind(phone,group);
    }

    @Override
    public void onBindGroupViewHolder(OSViewHolder holder, int flatPosition, ExpandableGroup group) {
        holder.setGroupName(group);
    }
}
    The last step is creating a running activity. In it's xml file, please put a RecyclerView object to this layout:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="info.devexchanges.expandablerecyclerview.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</RelativeLayout>
    In the Java code, it have no important point except set up LayoutManager for RecyclerView. Moreover, if you want to save/restore it's expand/collapse state, please overriding onSaveInstanceState() and onRestoreInstanceState() method.
Sour code for this activity:
MainActivity.java
package info.devexchanges.expandablerecyclerview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;

import info.devexchanges.expandablerecyclerview.adapter.RecyclerAdapter;
import info.devexchanges.expandablerecyclerview.model.MobileOS;
import info.devexchanges.expandablerecyclerview.model.Phone;

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private ArrayList<MobileOS> mobileOSes;
    private RecyclerAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mobileOSes = new ArrayList<>();

        setData();
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);

        adapter = new RecyclerAdapter(this, mobileOSes);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        adapter.onSaveInstanceState(outState);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        adapter.onRestoreInstanceState(savedInstanceState);
    }

    private void setData() {
        ArrayList<Phone> iphones = new ArrayList<>();
        iphones.add(new Phone("iPhone 4"));
        iphones.add(new Phone("iPhone 4S"));
        iphones.add(new Phone("iPhone 5"));
        iphones.add(new Phone("iPhone 5S"));
        iphones.add(new Phone("iPhone 6"));
        iphones.add(new Phone("iPhone 6Plus"));
        iphones.add(new Phone("iPhone 6S"));
        iphones.add(new Phone("iPhone 6S Plus"));

        ArrayList<Phone> nexus = new ArrayList<>();
        nexus.add(new Phone("Nexus One"));
        nexus.add(new Phone("Nexus S"));
        nexus.add(new Phone("Nexus 4"));
        nexus.add(new Phone("Nexus 5"));
        nexus.add(new Phone("Nexus 6"));
        nexus.add(new Phone("Nexus 5X"));
        nexus.add(new Phone("Nexus 6P"));
        nexus.add(new Phone("Nexus 7"));

        ArrayList<Phone> windowPhones = new ArrayList<>();
        windowPhones.add(new Phone("Nokia Lumia 800"));
        windowPhones.add(new Phone("Nokia Lumia 710"));
        windowPhones.add(new Phone("Nokia Lumia 900"));
        windowPhones.add(new Phone("Nokia Lumia 610"));
        windowPhones.add(new Phone("Nokia Lumia 510"));
        windowPhones.add(new Phone("Nokia Lumia 820"));
        windowPhones.add(new Phone("Nokia Lumia 920"));

        mobileOSes.add(new MobileOS("iOS", iphones));
        mobileOSes.add(new MobileOS("Android", nexus));
        mobileOSes.add(new MobileOS("Window Phone", windowPhones));
    }
}
    Running application, we'll have this output:

Conclusions

    Now, I've just presented the basic features of expandable-recycler-view library to make an expandable list view by using RecyclerView. Of course, you can realize that my post don't provide the way to handle child view click event. Okey, the fact that this library has an another module called expandablecheckrecyclerview which provide the checkable child views (bot single and multi check mode) and handling the child view click event with  onCheckChildCLick interface, you can go to it's Github page to read and find out the way to use it.
    References:

Share


Previous post
« Prev Post
Next post
Next Post »