RecyclerView二级列表

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

二级列表动图

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

需要了解

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

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<VH>
        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<VH>
        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<Unit<K,V>>了。

数据体有了,之前考虑到二级列表是两种不同的数据和不同的视图,需要不同的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

  • 5
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Paging3 是一个用于 Android 的分页库,它可以帮助你更加容易地实现分页功能。 要在 RecyclerView 中实现二级列表,你需要做以下几个步骤: 1. 创建一个数据类,用于保存二级列表的数据。 ```kotlin data class SubItem(val name: String, val description: String) data class Item(val name: String, val subItems: List<SubItem>) ``` 2. 创建一个 RecyclerViewAdapter 类,继承自 PagingDataAdapter。 ```kotlin class MyAdapter : PagingDataAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = getItem(position) if (item != null) { when (holder) { is ItemViewHolder -> holder.bind(item) is SubItemViewHolder -> holder.bind(item.subItems[position - 1]) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { ITEM_VIEW_TYPE -> ItemViewHolder.create(parent) SUB_ITEM_VIEW_TYPE -> SubItemViewHolder.create(parent) else -> throw IllegalArgumentException("unknown view type $viewType") } } override fun getItemViewType(position: Int): Int { return if (position == 0) { ITEM_VIEW_TYPE } else { SUB_ITEM_VIEW_TYPE } } companion object { private const val ITEM_VIEW_TYPE = 0 private const val SUB_ITEM_VIEW_TYPE = 1 } } ``` 3. 创建 ViewHolder 类。 ```kotlin class ItemViewHolder private constructor(private val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(item: Item) { binding.name.text = item.name } companion object { fun create(parent: ViewGroup): ItemViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val binding = ItemBinding.inflate(layoutInflater, parent, false) return ItemViewHolder(binding) } } } class SubItemViewHolder private constructor(private val binding: SubItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(subItem: SubItem) { binding.name.text = subItem.name binding.description.text = subItem.description } companion object { fun create(parent: ViewGroup): SubItemViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val binding = SubItemBinding.inflate(layoutInflater, parent, false) return SubItemViewHolder(binding) } } } ``` 4. 创建 DiffCallback。 ```kotlin object ItemDiffCallback : DiffUtil.ItemCallback<Item>() { override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { return oldItem.name == newItem.name } override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return oldItem == newItem } } ``` 5. 在 Activity 中初始化 RecyclerView 和 Paging3。 ```kotlin class MyActivity : AppCompatActivity() { private val viewModel by viewModels<MyViewModel>() private val adapter = MyAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.recyclerView.adapter = adapter lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> adapter.submitData(pagingData) } } } } ``` 6. 在 ViewModel 中创建数据源。 ```kotlin class MyViewModel : ViewModel() { private val repository = MyRepository() val flow = Pager(PagingConfig(pageSize = 10)) { repository.getPager() }.flow } ``` 7. 在 Repository 中创建数据源。 ```kotlin class MyRepository { fun getPager(): PagingSource<Int, Item> { return object : PagingSource<Int, Item>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> { val page = params.key ?: 1 val items = mutableListOf<Item>() // TODO: load data from API or database return LoadResult.Page( data = items, prevKey = if (page == 1) null else page - 1, nextKey = if (items.isEmpty()) null else page + 1 ) } override fun getRefreshKey(state: PagingState<Int, Item>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } } } } ``` 这样就完成了 RecyclerView二级列表实现。通过 Paging3,你可以更加方便地实现分页功能,同时也可以更好地管理数据源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值