RecyclerView二级列表

最近正好有做到二级列表,就记载一下怎样使用RecyclerView做二级列表吧。

效果大概就是这个样子,可以凑合用,主要是弄清楚大概原理,这样就知道步骤。代码地址在最下面。

需要了解

我们知道,写一个RecyclerView,需要配一个Adaper,一般是继承RecyclerView.Adapter此类来定制自己所需要。而这个类所必须实现的方法有三个:

public class TestAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
	@NonNull
	@Override
	public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
		return null;
	}

	@Override
	public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {

	}

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

接下来我们需要知道为什么实现这三个方法,其中的参数代表什么意义,其实就能对整体流程有个大致了解。

  • onCreateViewHolder
    • 此方法顾名思义是为了创建一个View的持有者,ViewHolder为整个列表视图寻找对应的View提供了依据
    • 传入的参数有两个,ViewGroup与viewType,viewGroup不必说,就是RecyclerView本身;而viewType是指View本身所定义的类型,默认是为0的,也就是说如果不去刻意修改,列表中所有视图默认都是一种类型—二级列表明显需要关注viewType,因为明显一级视图与二级视图一般是有所区别的
//RecyclerView.java$RecyclerView.Adapter
        public int getItemViewType(int position) {
            return 0;
        }

如果不重写此方法,我们拿到viewType就默认为0。在源码中,onCreateViewHolder方法传入的type,正是上面这个方法拿到的值:

                int offsetPosition;
                int type;
                
 				//省略部分代码

                    type = RecyclerView.this.mAdapter.getItemViewType(offsetPosition);
                    //省略部分代码 

                        holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type);

createViewHolder方法就调用需要重写的onCreateViewHolder方法,并传入参数:

        public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
            try {
                TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
                final VH holder = onCreateViewHolder(parent, viewType);
                if (holder.itemView.getParent() != null) {
                    throw new IllegalStateException("ViewHolder views must not be attached when"
                            + " created. Ensure that you are not passing 'true' to the attachToRoot"
                            + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
                }
                holder.mItemViewType = viewType;
                return holder;
            } finally {
                TraceCompat.endSection();
            }
        }
  • onBindViewHolder
    • 这个方法也比较好理解,通过onCreateViewHolder创建出ViewHolder之后,item的布局视图已经被加载到ViewHolder中了,意味着这个时候其实item已经可以显示了,只是如果布局有更多的次级控件,需要使用view.findViewById类似的方式去找到各自的view然后自行设定视图
//RecyclerView.java
    public abstract static class ViewHolder {
        @NonNull
        public final View itemView;
        WeakReference<RecyclerView> mNestedRecyclerView;
        int mPosition = -1;
        int mOldPosition = -1;
        long mItemId = -1L;
        int mItemViewType = -1;
        int mPreLayoutPosition = -1;
        RecyclerView.ViewHolder mShadowedHolder = null;
        RecyclerView.ViewHolder mShadowingHolder = null;
        //省略部分
        RecyclerView mOwnerRecyclerView;

        public ViewHolder(@NonNull View itemView) {
            if (itemView == null) {
                throw new IllegalArgumentException("itemView may not be null");
            } else {
                this.itemView = itemView;
            }
        }
}

查看ViewHolder的源码,可以知道itemView是已经有值的了。

//RecyclerView.java$RecyclerView.Adapter
        public abstract void onBindViewHolder(@NonNull VH var1, int var2);

        public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) {
            this.onBindViewHolder(holder, position);
        }

        public final void bindViewHolder(@NonNull VH holder, int position) {
            holder.mPosition = position;
            if (hasStableIds()) {
                holder.mItemId = getItemId(position);
            }
            holder.setFlags(ViewHolder.FLAG_BOUND,
                    ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
            TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
            holder.clearPayload();
            final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
            if (layoutParams instanceof RecyclerView.LayoutParams) {
                ((LayoutParams) layoutParams).mInsetsDirty = true;
            }
            TraceCompat.endSection();
        }

简而言之,bindView这一步骤是为了做更多细化的操作,包括得到更多次级控件,以及为各种view加载监听器之类的操作。

  • getItemCount
    • 这个方法是确定视图现有的数量,以确定绘制多少子view,基本是列表等集合视图必备的

需要考虑

啰嗦这么多,如果我们要做一个二级列表,考虑的东西主要有以下几点:

  1. 一级视图与二级视图如果视图有区别,或者直接一点说加载不是同一个layout文件,那么就需要区分itemViewType,也就是需要重写getItemViewType以区分view
  2. 既然要区别view,那么肯定最好是创建不同的ViewHolder以持有各种类型的view
  3. 如果二级可以折叠与伸展,那么对于整个视图来说,视图数量是在变化的,也就是getItemCount方法需要一定的计算
  4. 二级列表的数据一般是一对多,可能需要一个专有的数据体来存放这种一对多的数据,也需要依靠这种数据体来计算真正的数量

实现

先不论视图,为了二级列表的数据考虑,我们需要建立一个数据体以存放一对多的数据,那么简单点:

	public static class Unit<K, V> {
		public K group;
		public List<V> children;
		public boolean folded = true;

		public Unit(K group, List<V> children) {
			this.group = group;
			if (children == null) {
				this.children = new ArrayList<>();
			} else {
				this.children = children;
			}
		}

		public Unit(K group, List<V> children, boolean folded) {
			this(group, children);
			this.folded = folded;
		}
	}

就这几个属性就应该足够了,如果觉得folded这个属性耦合太强,可以另外新建一个类专门用于保存数据是否折叠的状态等参数。以图简便,这里直接加在了数据体中。
那么传入Adapter的数据就是各种List>了。

数据体有了,之前考虑到二级列表是两种不同的数据和不同的视图,需要不同的ViewHolder,那么就直接用类来区分好了:

	protected static abstract class FoldableViewHolder extends RecyclerView.ViewHolder {

		static final int GROUP = 0;
		static final int CHILD = 1;

		private SparseArray<View> views = new SparseArray<>();
		private View convertView;

		public FoldableViewHolder(@NonNull View itemView) {
			super(itemView);
			this.convertView = itemView;
		}

		@SuppressWarnings("unchecked")
		public <T extends View> T getView(int resId) {
            View v = views.get(resId);
			if (null == v) {
                v = convertView.findViewById(resId);
                views.put(resId, v);
			}
			return (T) v;
		}
	}

	protected static class GroupViewHolder extends FoldableViewHolder {

		public GroupViewHolder(@NonNull View itemView) {
			super(itemView);
		}

	}

	protected static class ChildViewHolder extends FoldableViewHolder {

		public ChildViewHolder(@NonNull View itemView) {
			super(itemView);
		}
	}

直接继承RecyclerView的ViewHolder,然后加个常量以方便之后对type的判断。

ViewHolder有了,重写onCreateViewHolder方法,以创建不同的ViewHolder:

	private Context mContext;

	/**
     * 上级布局
     */
	private int mGroupLayoutRes;
	/**
     * 下级布局
     */
	private int mChildLayoutRes;

	public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
		if (viewType == FoldableViewHolder.CHILD) {
			return new ChildViewHolder(LayoutInflater.from(mContext).inflate(mChildLayoutRes, viewGroup, false));
		}
		return new GroupViewHolder(LayoutInflater.from(mContext).inflate(mGroupLayoutRes, viewGroup, false));
	}

传入的是两种布局资源文件,因为这里的viewType已经被确定,直接通过个viewType来判断就可以了。

至此,基本工作已经准备好了,接下来需要对viewType及itemCount的确定进行一番逻辑判断与计算了。

首先是itemCount,这样写:

	private int mSize = 0;

	/**
	 * 数据
	 */
	private List<Unit<K, V>> mData;
	
	@Override
	public int getItemCount() {
		if (mSize == 0) {
			int totalSize = 0;
			for (Unit unit : mData) {
                totalSize += (unit.folded ? 1 : unit.children.size() + 1);
			}
            mSize = totalSize;
		}
//		System.out.println("itemCount="+mSize);
		return mSize;
	}

这里使用一个mSize用于记忆上次的itemCount,以避免每次都要重新计算,因为getItemCount是会被反复调用—不过使用这种方法,就需要在合适的时机重置mSize以便重新计算(主要是折叠状态变化时)。
这里判断的逻辑也很简单,利用Unit的数据体的属性,累加其数量,就能得到所有需要显示的数据数量,也就是当前视图的总数量。

再来是判断viewType,因为getViewType方法本身只传入的position参数,所以需要结合position也就是索引位置进行一定的计算与判断才行:

	@Override
	public int getItemViewType(int position) {
		//通过位置判断type,因为数据传入后顺序不变,可通过数据来判断当前位置是哪一类数据
		int currentPosition = -1;
		for (Unit unit : mData) {
			if (unit.folded) {
				//遍历到这里,如果是折叠状态,那么这个索引位置上的肯定是一级数据
                currentPosition = currentPosition + 1;
				if (currentPosition == position) {
					return FoldableViewHolder.GROUP;
				}
			} else {
				//伸展状态下,也得算上group的数量为1,索引位置如果相符则是对应类型数据
                currentPosition = currentPosition + 1;
				if (currentPosition == position) {
					return FoldableViewHolder.GROUP;
				}
				//算上children,通过比较大小确定是否是当前Unit中的child
				//如果不是,则进入下一次循环,判断下一个Unit
                currentPosition = currentPosition + unit.children.size();
				if (position <= currentPosition) {
					return FoldableViewHolder.CHILD;
				}
			}

		}
		return FoldableViewHolder.GROUP;
	}

判断是一级数据的逻辑很简单,因为不论是不是在伸展状态下,只要当前Unit的第一位置的索引与传入的索引相等,立即可以判定是一级数据;
判断二级数据主要是需要注意索引的边界,必须要在“如果不是当前Unit的一级数据”的前提下,才能进行后续的判断,这个索引有可能是二级数据的最后一个,那么也是二级数据,如果比最后一个二级数据的索引还要大,那么肯定是下一个Unit中的数据…依此类推。

最后就是一些关于视图变化的方法了,一般写在onBindViewHolder中:

	@Override
	public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, int position) {

       viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
//				System.out.println("click="+viewHolder.getAdapterPosition());
				if (viewHolder instanceof GroupViewHolder) {
                    Unit<K,V> unit = getUnit(viewHolder.getAdapterPosition());
                    unit.folded = !unit.folded;
                    mSize = 0;
//					notifyDataSetChanged();//最准确,但数据多时性能有影响
//					notifyItemRangeChanged(viewHolder.getAdapterPosition()+1,getItemCount());//需要考虑到holder的旧索引问题,暂无太好的办法去规避
					if(unit.folded){
						notifyItemRangeRemoved(viewHolder.getAdapterPosition()+1,unit.children.size());
					}else{
						notifyItemRangeInserted(viewHolder.getAdapterPosition()+1,unit.children.size());
					}
				}				
			}
		});

		//其他开发者实现的逻辑
	}

这里主要是内置一个点击事件—即点击一级数据,使折叠状态发生变化。为了使其重新计算itemCount将mSize重置了,另外就是数据变化时,需要使用notifyXXX之类的方法,以通知RecyclerView重新适应与绘制数据。

总体来说,与绘制一般列表的不同只在于计算itemCount与区别viewType而已,当然实际使用中也经常会用到通过索引查询数据的情况,也就是类似ListView中getItem的方法,这个也需要一定的计算,在例子中也有实现。
可以直接看例子中的代码。

此例代码地址,以供参考----foldablerecyclerview

你可能感兴趣的:(Android)