用RecyclerView实现N级树形列表

本文介绍了如何使用RecyclerView实现三级树形列表,通过数据处理和动态插入删除子列表,实现点击展开和折叠效果。在处理服务端返回的数据时,增加了深度和展开状态字段,通过递归设置每个元素的属性。点击展开时,将子列表添加到父列表中,折叠时则移除相应子列表。这种方式利用了RecyclerView的局部刷新特性,提高了代码的灵活性。
摘要由CSDN通过智能技术生成

最近在做项目的时候,需要实现一个章节树的功能。设计图大致类似这样

所谓树形列表,即是在父元素中包含子元素,当点击父元素的时候进行展开子元素,再次点击时收起子元素。且树形列表往往有多个层级。比较典型的情况就是计算机中的文件系统以及书籍中的目录这两种场景。

在我的项目场景中仅仅是展示一个三级列表,这样的话其实可以选择三个RecyclerView嵌套的方式,采用这种方式实现的优点是思考起来简单,容易理解。但是缺点是对RecyclerView需要有较多的控制,并且在项目迭代中不够灵活。

因此我认为采用一个RecyclerView实现更优雅一些,只用对一个View进行操作并且可以支持适配,理论上可以实现N级列表,只要N在计算机允许的范围内。即便这种实现方式需要对原始的数据进行处理。而且这种实现方式对于RecyclerView的特点进行了较好了利用,在展开与收起子元素的时候可以很好利用RecyclerView支持局部刷新的特点。

接下来先看一下我们的数据模型,在项目实际开发中,数据一般由服务端返回。而这种树形列表的数据模型一般是这样

data class ChapterSelectorModel(
    val id: Int,
    val name: String,
    val childlist: List<ChapterSelectorModel>?
)

 服务端返回的数据一般只会包含必要的信息,但是我们要展示与隐藏子元素的话,需要知道数据中的父子关系,这就要求我们对返回的数据进行必要的处理。比如变成这样

data class ChapterSelectorModel(
    val id: Int,
    val name: String,
    var depth: Int,
    var isExpanded: Boolean,
    val childlist: List<ChapterSelectorModel>?
)

其实就是增加必要的信息来作为父子判断的标准,我这里加入了深度与展开状态,当然你也加入是否叶子节点,是否有子孩子等等。

因为数据是从服务端获取,我们写一个全量的数据模型去接受服务端数据就可以,并不需要多写一个数据模型。

拿到之后我们需要递归的将每个元素的深度算出来并给展开状态赋值。

  private fun reSetData(chapters: List<ChapterSelectorModel>?, depth: Int) {
        if (chapters == null || chapters.isEmpty()) {
            return
        }
        for (chapter in chapters) {
            chapter.depth = depth
            chapter.isExpanded = false
            if (chapter.childlist != null && chapter.childlist.isNotEmpty()) {
                reSetData(chapter.childlist, depth + 1)
            }
        }
    }

在拿到数据的地方调用这个函数,深度初始值传入0即可。这样就会得到每个元素的深度值。

之后将新的数据传递给我们RecyclerView的Adapter即可。不过现在只是拿到了新的数据,但对于RecyclerView来讲,它只能展示一个列表的数据,即第一级列表,如何能让它实现多级列表的需求还需要我们再思考一下。

我们先讨论点击展开的情况,我们在点击第一级列表的子项时,需要展示一下该子项下的列表内容,那么是不是可以再点击的时候将该子项的列表加入父列表中就可以了。按照这个思路就很容易做到点击展开的效果了,这里也刚好有一个字段用于判断该Item是否展开。展开的代码如下

  private fun onOpen(itemData: ChapterSelectorModel, position: Int) {
        if (itemData.childlist != null && itemData.childlist.isNotEmpty()) {
            chapterSelectorData.addAll(position + 1, itemData.childlist)
            itemData.isExpanded = true
            notifyItemRangeInserted(position + 1, itemData.childlist.size)
            notifyItemChanged(position)
        }
    }

可以看到,将该元素的子元素添加到本身位置的后面,将展开状态置为true。但是对插入的条目进行局部刷新以及由于展开状态的变化对本身需要进行刷新。

然后再讨论一下折叠的情况,首先第一想法就是找到下一个和当前item深度相同的元素,用next标记,这样我们将当前item和next之间的元素从当前展示列表中删除即可。当然由于子元素也可能是展开的,因此需要递归的将子项的所有展开的列表从父列表中删除,并将子项中的展开状态置为false。折叠代码如下

    private fun onClose(itemData: ChapterSelectorModel, position: Int) {
        if (itemData.childlist != null && itemData.childlist.isNotEmpty()) {
            var next = chapterSelectorData.size - 1
            if (chapterSelectorData.size > position + 1) {
                for (i in position + 1 until chapterSelectorData.size) {
                    if (chapterSelectorData[i].depth <= chapterSelectorData[position].depth) {
                        next = i - 1
                        break
                    }
                }
                closeChild(chapterSelectorData[position])
                if (next > position) {
                    chapterSelectorData.subList(position + 1, next + 1).clear()
                    itemData.isExpanded = false
                    notifyItemRangeRemoved(position + 1, next - position)
                    notifyItemChanged(position)
                }
            }
        }
    }

 private fun closeChild(itemData: ChapterSelectorModel) {
        if (itemData.childlist != null) {
            for (child in itemData.childlist) {
                child.isExpanded = false
                closeChild(child)
            }
        }
    }

下面是完整的Adapter代码。

class ChapterTreeAdapter(
    private val context: Context,
    private val chapterSelectorData: MutableList<ChapterSelectorModel>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    inner class ViewHolder(val binding: ChapterTreeItemBinding) :
        RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val binding =
            ChapterTreeItemBinding.inflate(LayoutInflater.from(context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is (ViewHolder)) {
            val itemData = chapterSelectorData[holder.absoluteAdapterPosition]
            holder.binding.chapterItem.text = itemData.name
            if (itemData.childlist != null && itemData.childlist.isNotEmpty()) {
                holder.binding.chapterItemState.visibility = View.VISIBLE
                if (itemData.isExpanded) {
                    holder.binding.chapterItemState.setImageResource(R.drawable.list_close_icon)
                } else {
                    holder.binding.chapterItemState.setImageResource(R.drawable.list_open_icon)
                }
            } else {
                holder.binding.chapterItemState.visibility = View.INVISIBLE
            }
            val left = (itemData.depth) * 5
            holder.binding.chapterItem.setPadding(left * 12, 0, 0, 0)
            holder.itemView.setOnClickListener {
                if (itemData.isExpanded) {
                    onClose(itemData, holder.getAbsoluteAdapterPosition())
                } else {
                    onOpen(itemData, holder.getAbsoluteAdapterPosition())
                }
            }
        }
    }

    override fun getItemCount() = chapterSelectorData.size

    private fun onClose(itemData: ChapterSelectorModel, position: Int) {
        if (itemData.childlist != null && itemData.childlist.isNotEmpty()) {
            var next = chapterSelectorData.size - 1
            if (chapterSelectorData.size > position + 1) {
                for (i in position + 1 until chapterSelectorData.size) {
                    if (chapterSelectorData[i].depth <= chapterSelectorData[position].depth) {
                        next = i - 1
                        break
                    }
                }
                closeChild(chapterSelectorData[position])
                if (next > position) {
                    chapterSelectorData.subList(position + 1, next + 1).clear()
                    itemData.isExpanded = false
                    notifyItemRangeRemoved(position + 1, next - position)
                    notifyItemChanged(position)
                }
            }
        }
    }

    private fun onOpen(itemData: ChapterSelectorModel, position: Int) {
        if (itemData.childlist != null && itemData.childlist.isNotEmpty()) {
            chapterSelectorData.addAll(position + 1, itemData.childlist)
            itemData.isExpanded = true
            notifyItemRangeInserted(position + 1, itemData.childlist.size)
            notifyItemChanged(position)
        }
    }

    private fun closeChild(itemData: ChapterSelectorModel) {
        if (itemData.childlist != null) {
            for (child in itemData.childlist) {
                child.isExpanded = false
                closeChild(child)
            }
        }
    }
}

大家根据自己的实际需求添加自己的逻辑即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值