最近正好有做到二级列表,就记载一下怎样使用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;
}
}
接下来我们需要知道为什么实现这三个方法,其中的参数代表什么意义,其实就能对整体流程有个大致了解。
//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();
}
}
//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加载监听器之类的操作。
啰嗦这么多,如果我们要做一个二级列表,考虑的东西主要有以下几点:
先不论视图,为了二级列表的数据考虑,我们需要建立一个数据体以存放一对多的数据,那么简单点:
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