ExpanableRecyclerView

demo

Github:github.com/hgDendi/Exp…

Custom support secondary menu RecyclerViewAdapter.

Packages will open closed operations in the BaseExpandableRecyclerViewAdapter, make the whole way of using full of elasticity.

Below are the specific usage methods. Generally, override is required for 6 methods:

  • getGroupCount
  • getGroupItem
  • onCreateGroupViewHolder
  • onCreateChildViewHolder
  • onBindGroupViewHolder
  • onBindChildViewHolder

Since onCreateViewHolder and onBindViewHolder are the RecyclerViewAdapter methods that need to force Override, they are split into two methods according to the parent-child relationship. However, getGroupCount and getGroupItem can be realized by a single line of code based on List in high probability, so they are very easy to use.

Gradle

dependencies{
    compile 'com. HgDendi: expandable recyclerview - adapter: 1.0.1'
}Copy the code

advantages

  1. Easy to use, simple and clear
  2. Retain the original mechanism of RecyclerView to the maximum extent, slide to specific items before rendering, will not slide to another Group to render all sub-items under another Group
  3. Partial refresh of itemView. You can customize the refresh mechanism for expanded and closed itemView to avoid refreshing the entire GroupItem when expanded and closed (e.g. simple arrow pointing to change).
  4. Using generics, user – defined input parameters, higher scalability

Method of use

Define parent-child data structures

The GroupBean needs to inherit from BaseGroupBean and override three methods.

  • getChildCount
    • Obtain the number of child nodes
  • isExpandable
    • Whether it is an expandable node
    • The default implementation can be to determine whether the child node is zero, but you can do other things as well
  • getChildAt
    • Obtain the corresponding child node data structure according to index
class SampleGroupBean implements BaseExpandableRecyclerViewAdapter.BaseGroupBean<SampleChildBean> {
    @Override
    public int getChildCount(a) {
        return mList.size();
    }

    // whether this group is expandable
    @Override
    public boolean isExpandable(a) {
        return getChildCount() > 0;
    }

      @Override
    public SampleChildBean getChildAt(int index) {
        return mList.size() <= index ? null: mList.get(index); }}public class SampleChildBean {}Copy the code

Define the corresponding ViewHolder

The ViewHolder corresponding to the Group inherits BaseGroupViewHolder and overwrites onExpandStatusChanged.

This method is a method to realize the local refresh of item, which will be called back when expanded or closed. For example, in most cases, the switch closed state only needs to modify the left arrow pointing, without refreshing other parts of itemView.

The implementation principle is to use RecyclerView payload mechanism to realize local listening refresh.

static class GroupVH extends BaseExpandableRecyclerViewAdapter.BaseGroupViewHolder {
    GroupVH(View itemView) {
        super(itemView);
    }

    // this method is used for partial update.Which means when expand status changed,only a part of this view need to invalidate
    @Override
    protected void onExpandStatusChanged(RecyclerView.Adapter relatedAdapter, boolean isExpanding) {
      // 1. Update only the left open and closed arrows
      foldIv.setImageResource(isExpanding ? R.drawable.ic_arrow_expanding : R.drawable.ic_arrow_folding);

      // 2. Refresh the entire Item by defaultrelatedAdapter.notifyItemChanged(getAdapterPosition()); }}static class ChildVH extends RecyclerView.ViewHolder {
    ChildVH(View itemView) {
        super(itemView); }}Copy the code

Inherit the base class using a custom Adapter

/ /!!!!! Notice the generics used for inheritance here, the Bean and ViewHolder mentioned above
public class SampleAdapter extends BaseExpandableRecyclerViewAdapter
<SampleGroupBean.SampleChildBean.SampleAdapter.GroupVH.SampleAdapter.ChildVH>

    @Override
    public int getGroupCount(a){
        // Number of parent nodes
    }

    @Override
    public GroupBean getGroupItem(int groupIndex) {
        // Get the parent node
    }

    @Override
    public GroupVH onCreateGroupViewHolder(ViewGroup parent, int groupViewType) {}@Override
    public ChildVH onCreateChildViewHolder(ViewGroup parent, int childViewType) {}@Override
    public void onBindGroupViewHolder(GroupVH holder, SampleGroupBean sampleGroupBean, boolean isExpand) {}@Override
    public void onBindChildViewHolder(ChildVH holder, SampleGroupBean sampleGroupBean, SampleChildBean sampleChildBean) {}}Copy the code

Other USES

Increase the variety of fathers and sons

Override the getChildType and getGroupType methods to control type, which is passed back to onCreateGroupViewHolder and onCreateChildViewHolder.

protected int getGroupType(GroupBean groupBean) {
    return 0;
}

abstract public GroupViewHolder onCreateGroupViewHolder(ViewGroup parent, int groupViewType);

protected int getChildType(GroupBean groupBean, ChildBean childBean) {
    return 0;
}

abstract public ChildViewHolder onCreateChildViewHolder(ViewGroup parent, int childViewType);Copy the code

Add EmptyView when the list is empty

adapter.setEmptyViewProducer(new ViewProducer() {
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
        View emptyView = LayoutInflater.from(parent.getContext()).inflate(R.layout.empty, parent, false);
        return new DefaultEmptyViewHolder(emptyView);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder) {}});Copy the code

Increase the HeaderView

adapter.setEmptyViewProducer(new ViewProducer() {
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
        View emptyView = LayoutInflater.from(parent.getContext()).inflate(R.layout.header, parent, false);
        return new DefaultEmptyViewHolder(emptyView);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder) {}},false);Copy the code

Listen for an event

Listener events can be set using setListener.

public interface ExpandableRecyclerViewOnClickListener<GroupBean extends BaseGroupBean.ChildBean> {

        /** ** long time operation **@param groupItem
         * @return* /
        boolean onGroupLongClicked(GroupBean groupItem);

        /** * A callback that is triggered when the group is clicked, and returns a Boolean indicating whether to intercept the operation **@param groupItem
         * @param isExpand
         * @returnTrue invalidates a click. False Expand or close operations normally. * /
        boolean onInterceptGroupExpandEvent(GroupBean groupItem, boolean isExpand);

        /** * GroupView (isExpandable of the Group returns false to trigger this callback) **@param groupItem
         */
        void onGroupClicked(GroupBean groupItem);

        /** * Click on the child View **@param groupItem
         * @param childItem
         */
        void onChildClicked(GroupBean groupItem, ChildBean childItem);
    }Copy the code

Realize the principle of

Parent-child structure division

  1. GetItemType is used to determine the type, and the base class is divided into four types of Views.
private static final int TYPE_EMPTY = ViewProducer.VIEW_TYPE_EMPTY;
private static final int TYPE_HEADER = ViewProducer.VIEW_TYPE_HEADER;
private static final int TYPE_GROUP = ViewProducer.VIEW_TYPE_EMPTY >> 2;
private static final int TYPE_CHILD = ViewProducer.VIEW_TYPE_EMPTY >> 3;
private static final int TYPE_MASK = TYPE_GROUP | TYPE_CHILD | TYPE_EMPTY | TYPE_HEADER;

// Use getItemView to determine the type of MASK defined above by default. You can subclass Group and Child, but do not allow conflicts with TYPE_MASK, otherwise Exception will be reported
@Override
public final int getItemViewType(int position) {
    if (mIsEmpty) {
        return position == 0 && mShowHeaderViewWhenEmpty ? TYPE_HEADER : TYPE_EMPTY;
    }
    if (position == 0&& mHeaderViewProducer ! =null) {
        return TYPE_HEADER;
    }
    int[] coord = translateToDoubleIndex(position);
    GroupBean groupBean = getGroupItem(coord[0]);
    if (coord[1] < 0) {
        int groupType = getGroupType(groupBean);
        if ((groupType & TYPE_MASK) == 0) {
            return groupType | TYPE_GROUP;
        } else {
            throw new IllegalStateException(
                String.format(Locale.getDefault(), "GroupType [%d] conflits with MASK [%d]", groupType, TYPE_MASK)); }}else {
        int childType = getChildType(groupBean, groupBean.getChildAt(coord[1]));
        if ((childType & TYPE_MASK) == 0) {
            return childType | TYPE_CHILD;
        } else {
            throw new IllegalStateException(
                String.format(Locale.getDefault(), "ChildType [%d] conflits with MASK [%d]", childType, TYPE_MASK)); }}}Copy the code
  1. Type judgments are made in onCreateViewHolder and onBindViewHolder, calling different methods that are overridden in subclasses. Note that we make all three methods final to prevent subclasses from overloading, which can only override specific methods of different types.
@Override
public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    switch (viewType & TYPE_MASK) {
        case TYPE_EMPTY:
            return mEmptyViewProducer.onCreateViewHolder(parent);
        case TYPE_HEADER:
            return mHeaderViewProducer.onCreateViewHolder(parent);
        case TYPE_CHILD:
            return onCreateChildViewHolder(parent, viewType ^ TYPE_CHILD);
        case TYPE_GROUP:
            return onCreateGroupViewHolder(parent, viewType ^ TYPE_GROUP);
        default:
            throw new IllegalStateException(
                String.format(Locale.getDefault(), "Illegal view type : viewType[%d]", viewType)); }}@Override
public final void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
    onBindViewHolder(holder, position, null);
}

@Override
public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List<Object> payloads) {
    switch (holder.getItemViewType() & TYPE_MASK) {
        case TYPE_EMPTY:
            mEmptyViewProducer.onBindViewHolder(holder);
            break;
        case TYPE_HEADER:
            mHeaderViewProducer.onBindViewHolder(holder);
            break;
        case TYPE_CHILD:
            final int[] childCoord = translateToDoubleIndex(position);
            GroupBean groupBean = getGroupItem(childCoord[0]);
            bindChildViewHolder((ChildViewHolder) holder, groupBean, groupBean.getChildAt(childCoord[1]), payloads);
            break;
        case TYPE_GROUP:
            bindGroupViewHolder((GroupViewHolder) holder,
                getGroupItem(translateToDoubleIndex(position)[0]), payloads);
            break;
        default:
            throw new IllegalStateException(
                String.format(Locale.getDefault(), "Illegal view type : position [%d] ,itemViewType[%d]", position, holder.getItemViewType())); }}Copy the code

Open and close operation

operation

When the groupBean isExpandable returns true, set the click event for itemView to expand and close.

The specific principle of expansion and closure is to record expansion and closure in Set, update when expansion and closure occur, and use notifyItemChange interface to refresh the list locally.

private Set<GroupBean> mExpandGroupSet;

protected void bindGroupViewHolder(final GroupViewHolder holder, final GroupBean groupBean, List<Object> payload) {
      // ...
    if(! groupBean.isExpandable()) {// ...
    } else {
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final boolean isExpand = mExpandGroupSet.contains(groupBean);
                if (mListener == null| |! mListener.onInterceptGroupExpandEvent(groupBean, isExpand)) {final int adapterPosition = holder.getAdapterPosition();
                holder.onExpandStatusChanged(BaseExpandableRecyclerViewAdapter.this, !isExpand);
                    if (isExpand) {
                        mExpandGroupSet.remove(groupBean);
                        notifyItemRangeRemoved(adapterPosition + 1, groupBean.getChildCount());
                    } else {
                        mExpandGroupSet.add(groupBean);
                        notifyItemRangeInserted(adapterPosition + 1, groupBean.getChildCount()); }}}}); }// Subclass implementation
    onBindGroupViewHolder(holder, groupBean, isGroupExpand(groupBean));
}Copy the code

Local refresh principle

Payload is defined and partially refreshed using the Payload mechanism.

When notifyItemChange is passed in payload, the onBindViewHolder operation checks whether there is a payload. If there is a payload, the partial refresh operation is performed.

private static final Object EXPAND_PAYLOAD = new Object();

// The interface to call when partial refresh (wrapped, no need to be called by the user)
notifyItemChanged(position, EXPAND_PAYLOAD);

/ / processing content
protected void bindGroupViewHolder(final GroupViewHolder holder, final GroupBean groupBean, List<Object> payload) {
    if(payload ! =null&& payload.size() ! =0) {
        if (payload.contains(EXPAND_PAYLOAD)) {
              // The holder method has abstract methods in which concrete expansion and closure logic is implemented
            holder.onExpandStatusChanged(BaseExpandableRecyclerViewAdapter.this, isGroupExpand(groupBean));
            if (payload.size() == 1) {
                return;
            }
        }
    onBindGroupViewHolder(holder, groupBean, isGroupExpand(groupBean), payload);
    return; }}Copy the code

License

MIT

rem.mit-license.org/