这里说的树形结构,指的是分父级子级元素。点击父级,可以展开或者隐藏子级,且父子一共有多级(甚至无限层级,但现实中一般没有这种情况)。现实中的场景,首先能想到的是文件管理器应用,随着目录层级一层层展开。还有一个常见的应用,就是学科-知识点或大课程包含子课程,也就是很多在线教育类APP的题库和课程表中能看到的。
先看一下效果:
显然,实现多级树效果,不需要对原生的RecyclerView做什么改动,只要实现一个适当的Adapter就足够了。下面就看看实现上面GIF图的代码吧。
首先,写一个简单的数据模型类:
public class TreeItem { public String title; public int itemLevel; public int itemState; public List<TreeItem> child; }
其中,itemLevel用来记录该元素的级别(从0开始是最高级,然后依次为1、2等)。itemState用来记录元素的状态,即是打开还是合并的状态。child则是展开后增加显示的元素列表,实际上就是把Adapter的数据list进行add操作的参数。
再来一个树的点击处理接口:
public interface TreeStateChangeListener { void onOpen(TreeItem treeItem, int position); void onClose(TreeItem treeItem, int position); }
2个方法,分别处理展开和合并的事件,TreeItem参数很好理解,就是被点击位置的元素数据,而position也就是元素在RecyclerView列表中的数据,直接通过getAdapterPosition获取后传过来,比在List里通过元素获取位置效率要高得多。
然后就是重头戏Adapter了,布局很简单就不在文中展示了,主体是一个指示器(包括一个圆形的蓝色圆,一个显示+-号来显示展开合并状态的TextView),显示标题文字的TextView和一个底部的间隔线。
接下来就是Adapter的Java代码了,其实主要就在于onOpen和onClose2个方法的实现,以及初始化全部数据使其满足我们展开树的形式。
private void initList(List<TreeItem> list, int level) { if (list == null || list.size() <= 0) return; for (TreeItem item: list) { item.itemLevel = level; if (item.child != null && item.child.size() > 0) { initList(item.child, level + 1); } } }
初始化数据,会把全部数据进行遍历,并且level从0开始,依次增加,用来合并的时候判断最终的位置。
@Override public void onOpen(TreeItem treeItem, int position) { if (treeItem.child != null && treeItem.child.size() > 0) { mList.addAll(position + 1, treeItem.child); treeItem.itemState = ITEM_STATE_OPEN; notifyItemRangeInserted(position + 1, treeItem.child.size()); notifyItemChanged(position); } }
展开的时候比较简单,把该元素的child数据添加到本身位置的后面,并且把本元素的状态改为展开状态,然后使用RecyclerView特有的局部刷新,先通知在position+1的位置插入了一定条目的数据,并且把position位置也进行单独刷新(因为该位置的元素状态变成了打开)。
@Override public void onClose(TreeItem treeItem, int position) { if (treeItem.child != null && treeItem.child.size() > 0) { int nextSameOrHigherLevelNodePosition = mList.size() - 1; if (mList.size() > position + 1) { for (int i = position + 1; i < mList.size(); i++) { if (mList.get(i).itemLevel <= mList.get(position).itemLevel) { nextSameOrHigherLevelNodePosition = i - 1; break; } } closeChild(mList.get(position)); if (nextSameOrHigherLevelNodePosition > position) { mList.subList(position + 1, nextSameOrHigherLevelNodePosition + 1).clear(); treeItem.itemState = ITEM_STATE_CLOSE; notifyItemRangeRemoved(position + 1, nextSameOrHigherLevelNodePosition - position); notifyItemChanged(position); } } } } private void closeChild(TreeItem treeItem) { if(treeItem.child != null){ for (TreeItem child:treeItem.child) { child.itemState = ITEM_STATE_CLOSE; closeChild(child); } } }
onClose方法要复杂一些,因为相应位置的元素在合并的时候,不仅仅它的child级是展开的,甚至child元素的child(可以继续深入下去)也是展开的,所以需要把元素的任意级别的child元素全部从mList中删除。所以我定义了一个nextSameOrHigherLevelNodePosition变量(不要吐槽我的命名水平……),先把它的值定义为整个mList中最后一个元素的位置(如果整个mList里都没有比position元素更高级的,那么nextSameOrHigherLevelNodePosition的值就是mList.size() – 1了),然后通过循环,在mList里找到position之后的第一个级别不低于position的元素,nextSameOrHigherLevelNodePosition就是这个元素的位置了。然后通过closeChild方法继续递归一下,把所有层级的child元素的状态都改为合并的状态(要不然下次展开操作就混乱了)。接下来的操作就很容易理解了,把相应位置的元素删除,并进行通知刷新。
当然了,mList.subList(position + 1, nextSameOrHigherLevelNodePosition + 1).clear();这一行代码里有一定的玄机,感兴趣的可以自行研究。
全部代码在这里:https://github.com/QingLian/AndroidTreeRecyclerView
评论