RecyclerView实现多级树形控件,可以展开或折叠

KaelLi 2018年12月13日13:33:05
评论
12,7804

这里说的树形结构,指的是分父级子级元素。点击父级,可以展开或者隐藏子级,且父子一共有多级(甚至无限层级,但现实中一般没有这种情况)。现实中的场景,首先能想到的是文件管理器应用,随着目录层级一层层展开。还有一个常见的应用,就是学科-知识点或大课程包含子课程,也就是很多在线教育类APP的题库和课程表中能看到的。

先看一下效果:

RecyclerView实现多级树形控件,可以展开或折叠
以往要实现这种列表,除了ListView几乎没有别的选择(Google早期也没提供其他合适的控件)。使用ListView很简单,实现能展开和关闭的多级树思路也简单:展开就在相应的位置add数据,关闭就remove数据,然后notifyDataChanged。但这么做有个问题,当数据量较大时,每次点击(无论展开还是关闭)都要notifyDataChanged,这对性能的要求很高,很容易导致界面卡住不流畅甚至是ANR。这时候RecyclerView就可以派上用场了,无它,因为支持局部刷新嘛。

显然,实现多级树效果,不需要对原生的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

KaelLi
  • 本文由 发表于 2018年12月13日13:33:05
  • 转载请务必保留本文链接:https://www.kaelli.com/22.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: