Search code examples
androidandroid-recyclerviewandroid-viewholder

How to handle expandable ViewHolders in a RecyclerView?


I have a RecyclerView working with FirestoreRecyclerAdapter in my app. The ViewHolders implement an expandable 'quick actions' menu that expands on tapping the item, and the ideal behavior is to have only one item expanded at a time. Expanding one item should collapse all other items in the adapter.

When there are few enough items to fit them all on screen, this works fine. But if the list extends off-screen, when I try to iterate over the adapter's items, I get a NullPointerException thrown when the detached or off-screen ViewHolders try to call getParent().

How can I iterate over all ViewHolders in the adapter, or alternatively, only iterate over visible ones?

Relevant adapter code:

@Override
protected void onBindViewHolder(@NonNull BrewViewHolder holder, 
               int position, @NonNull Brew brew) {

    // Bind Views
    holder.bind(brew);

    // Set expander ClickListener
    holder.card.setOnClickListener(v -> {
        for (int i = 0; i < mAdapter.getItemCount(); i++) {
            if (i != position) {
                // ERROR THROWN HERE
                BrewViewHolder vh = ((BrewViewHolder) recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
                vh.expanded = false;
            }
        }
        holder.expanded = !holder.expanded;
        notifyItemChanged(position);
   });

}

Relevant ViewHolder code:

public class BrewViewHolder extends RecyclerView.ViewHolder {

    // Expanded state
    public boolean expanded = false;
    ...

    public BrewViewHolder(@NonNull final View itemView) {
        super(itemView);
        ...
        // Set visibility of quick actions based on expanded state
        quickActions.setVisibility(expanded ? View.VISIBLE : View.GONE);
    }

}

Thanks in advance!

EDIT: As Gilberto pointed out, this is very expensive. Went with a version of Andrew's solution:

private int expandedItemIndex = -1;

@Override
protected void onBindViewHolder(@NonNull BrewViewHolder holder, int position,
                                @NonNull Brew brew) {
    // Bind Views
    holder.bind(brew);

    // Set expander ClickListener
    holder.card.setOnClickListener(v -> {
        if (position == expandedItemIndex) {
            holder.expanded = false;
            expandedItemIndex = -1;
        } else {
            holder.expanded = true;
            if (expandedItemIndex != -1) {
                BrewViewHolder otherHolder = ((BrewViewHolder) recyclerView.getChildViewHolder(
                        recyclerView.getChildAt(expandedItemIndex)));
                otherHolder.expanded = false;
            }
            expandedItemIndex = position;
        }
        notifyItemRangeChanged(0, recyclerView.getChildCount());
    });

}

Solution

  • You should not iterate all views. You need just save index of expanded view and call notifyDataSetChanged or notifyItemChanged.

    private int expandedItemIndex = -1;
    
    @Override
    protected void onBindViewHolder(@NonNull BrewViewHolder holder, 
                   final int position, @NonNull Brew brew) {
    
        // Bind Views
        holder.bind(brew);
    
        // Set expander ClickListener
        holder.card.setOnClickListener(v -> {
            if (position == expandedItemIndex) {
                notifyItemChanged(position);
                expandedItemIndex = -1;
            } else {
                if (expandedItemIndex != -1) {
                    notifyItemChanged(expandedItemIndex);
                }
                expandedItemIndex = position;
                notifyItemChanged(position);
            }
       });
    
       if (position == expandedItemIndex) {
          // Expand
          holder.quickActions.setVisibility(View.VISIBLE);
       } else {
          // Collapse
          holder.quickActions.setVisibility(View.GONE);
       }
    }