RecyclerView provides several optimisations over ListView. But it doesn't provide an important component which ListView provides out-of-the-box. And that's the ExpandableListView. Many of us still require such a kind of design where headers can be expanded/collapsed to show/hide child views. In this post, we will look at an idea of how to implement this functionality using RecyclerView.
If you are not familiar with RecyclerView, you can go through my previous blog posts here and here.
Let's consider a list of employees who are categorised according to their designation. The designations and employees are shown using different view types. The designation view act as header and employee views act as children.
I have two Lists containing employees denoted by userList
and their designations denoted by userTypeList
respectively.
List usersList = usersData.getUsersList(); List userTypeList = usersData.getUserTypeList();
We have to change our Adapter class to handle the expand/collapse functionality. To keep track of the expand/collapse state I am using a SparseIntArray in the adapter where 0
represents collapsed state and 1
expanded state.
private SparseIntArray headerExpandTracker;
To keep track of the view type and its position in the respective Lists I am using a SparseArray
of ViewType
where ViewType
holds the data index and type as shown below.
public class ViewType { private int dataIndex; private int type; public ViewType(int dataIndex, int type) { this.dataIndex = dataIndex; this.type = type; } public int getDataIndex() { return dataIndex; } public int getType() { return type; } }
Now we will change the getItemCount()
method to get the number of items to display. Initially all the items will be in collapsed state. So only headers will be visible.
Here is the getItemCount():
@Override public int getItemCount() { int count = 0; if (userTypeList != null && usersList != null) { viewTypes.clear(); int collapsedCount = 0; for (int i = 0; i < userTypeList.size(); i++) { viewTypes.put(count, new ViewType(i, HEADER_TYPE)); count += 1; String userType = userTypeList.get(i); int childCount = getChildCount(userType); if (headerExpandTracker.get(i) != 0) { // Expanded for (int j = 0; j < childCount; j++) { viewTypes.put(count, new ViewType(count - (i + 1) + collapsedCount, USER_TYPE)); count += 1; } } else { // Collapsed collapsedCount += childCount; } } } return count; }
In the code, I am looping through the userTypeList
adding each of the header view type to the viewTypes
SparseArray. Then we check whether the header is expanded or not using this code:
if (headerExpandTracker.get(i) != 0) { // Expanded State } else { // Collapsed State }
If it is collapsed we are adding the count of the collapsed child views to the collapsedCount
. The number of children for a given user type will be calculated and returned in the getChildCount
method.
If it is expanded we are adding each child view type to the viewTypes
array. Note this line:
viewTypes.put(count, new ViewType(count - (i + 1) + collapsedCount, USER_TYPE));
Here the first parameter of the ViewType
class represents the index
of the data in the userList. We subtract the headers added from the count using (i + 1)
and adding the count of any collapsed views before this user view, using collapsedCount
.
Next in the getItemViewType()
we return the correct view type based on the position.
@Override public int getItemViewType(int position) { if (viewTypes.get(position).getType() == HEADER_TYPE) { return HEADER_TYPE; } else { return USER_TYPE; } }
Then in the onCreateViewHolder()
, based on the viewType
the correct view is inflated and the ViewHolder is returned.
After that, based on the view type, onBindViewHolder
binds the necessary data to the necessary view.
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { int itemViewType = getItemViewType(position); ViewType viewType = viewTypes.get(position); if (itemViewType == USER_TYPE) { bindUserViewHolder(holder, viewType); } else { bindHeaderViewHolder(holder, position, viewType); } }
private void bindHeaderViewHolder(RecyclerView.ViewHolder holder, int position, ViewType viewType) { int dataIndex = viewType.getDataIndex(); SectionHeaderViewHolder headerViewHolder = (SectionHeaderViewHolder) holder; headerViewHolder.sectionTitle.setText(userTypeList.get(dataIndex)); if (isExpanded(position)) { headerViewHolder.sectionTitle .setCompoundDrawablesWithIntrinsicBounds(null, null, headerViewHolder.arrowUp, null); } else { headerViewHolder.sectionTitle .setCompoundDrawablesWithIntrinsicBounds(null, null, headerViewHolder.arrowDown, null); } }Note thisprivate void bindUserViewHolder(RecyclerView.ViewHolder holder, ViewType viewType) { int dataIndex = viewType.getDataIndex(); ((UserViewHolder) holder).username.setText(usersList.get(dataIndex).getName()); Glide.with(holder.itemView).load(usersList.get(dataIndex).getImageUrl()).into(((UserViewHolder) holder).userAvatar); }
int dataIndex = viewType.getDataIndex();
in the code above. Using this dataIndex we will get the correct data from userTypeList
and userList
respectively.
The expand/collapse action is triggered from the Header ViewHolder's onClickListener. In the click listener, an interface function is called which is implemented by the Adapter. This function takes the adapter position as parameter.
@Override public void onHeaderClick(int position) { ViewType viewType = viewTypes.get(position); int dataIndex = viewType.getDataIndex(); String userType = userTypeList.get(dataIndex); int childCount = getChildCount(userType); if (headerExpandTracker.get(dataIndex) == 0) { // Collapsed. Now expand it headerExpandTracker.put(dataIndex, 1); notifyItemRangeInserted(position + 1, childCount); } else { // Expanded. Now collapse it headerExpandTracker.put(dataIndex, 0); notifyItemRangeRemoved(position + 1, childCount); } }
Here the headerExpandTracker
is checked to see if header is expanded or collapsed. If collapsed, it has to be expanded. The headerExpandTracker
value is changed and the adapter is notified about the insertion of the child views using notifyItemRangeInserted
passing the position of the first child view and the total count. Similarly if it's in expanded state, the headerExpandTracker
value is changed and notifyItemRangeRemoved()
is called.
After implementing all, this is how the RecyclerView looks like:
You can build upon this example for more complex and feature rich Expandable RecyclerView.
Full source code can be found here.
In the next posts I will cover more use-cases with RecyclerView.