You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Android中利用RecyclerView动态构建嵌套树形视图结构?

Android RecyclerView树形视图实现方案(子项数量未知场景)

当然可以用单一布局配合统一的适配器实现,甚至不需要嵌套RecyclerView(嵌套反而容易引发滚动冲突、性能冗余问题),下面给你两种实用方案:

方案1:扁平列表模拟树形(推荐)

核心思路是把递归的树形数据转换成扁平的一维列表,通过缩进和展开/折叠操作动态维护列表内容,整个RecyclerView只用一个适配器。

1. 定义统一数据模型

创建一个支持递归的节点类,包含层级、子项、展开状态:

data class TreeNode(
    val id: String,
    val content: String,
    val level: Int, // 层级,用于设置缩进
    var isExpanded: Boolean = false,
    val children: MutableList<TreeNode> = mutableListOf()
)

2. 单一列表项布局

布局文件item_tree_node.xml里不需要嵌套RecyclerView,只需要内容区、展开按钮:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:paddingVertical="8dp">

    <!-- 缩进占位,根据层级动态设置宽度 -->
    <View
        android:id="@+id/v_indent"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

    <!-- 展开/折叠按钮 -->
    <ImageView
        android:id="@+id/iv_expand"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:src="@drawable/ic_expand_more"/>

    <!-- 内容文本 -->
    <TextView
        android:id="@+id/tv_content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="8"
        android:paddingHorizontal="8dp"/>
</LinearLayout>

3. 适配器实现(核心逻辑)

适配器里维护一个扁平列表,处理展开/折叠时的列表更新:

class TreeAdapter(private val flatNodeList: MutableList<TreeNode>) :
    RecyclerView.Adapter<TreeAdapter.TreeViewHolder>() {

    inner class TreeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val indentView: View = itemView.findViewById(R.id.v_indent)
        val expandIv: ImageView = itemView.findViewById(R.id.iv_expand)
        val contentTv: TextView = itemView.findViewById(R.id.tv_content)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TreeViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_tree_node, parent, false)
        return TreeViewHolder(view)
    }

    override fun onBindViewHolder(holder: TreeViewHolder, position: Int) {
        val node = flatNodeList[position]
        // 根据层级设置缩进宽度(比如每层缩进24dp)
        val indentWidth = node.level * 24 * holder.itemView.resources.displayMetrics.density
        holder.indentView.layoutParams.width = indentWidth.toInt()
        // 设置内容
        holder.contentTv.text = node.content
        // 控制展开按钮显示:没有子项则隐藏
        holder.expandIv.visibility = if (node.children.isEmpty()) View.GONE else View.VISIBLE
        // 设置按钮图标
        holder.expandIv.setImageResource(
            if (node.isExpanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more
        )

        // 处理展开/折叠点击
        holder.expandIv.setOnClickListener {
            node.isExpanded = !node.isExpanded
            if (node.isExpanded) {
                // 展开:将子项插入到当前项的下一个位置
                val insertPosition = position + 1
                flatNodeList.addAll(insertPosition, node.children)
                notifyItemRangeInserted(insertPosition, node.children.size)
            } else {
                // 折叠:递归移除所有子项(包括子项的子项)
                var removeCount = 0
                val iterator = flatNodeList.listIterator(position + 1)
                while (iterator.hasNext()) {
                    val childNode = iterator.next()
                    if (childNode.level <= node.level) break
                    removeCount++
                    iterator.remove()
                }
                notifyItemRangeRemoved(position + 1, removeCount)
            }
            // 更新当前项图标
            notifyItemChanged(position)
        }
    }

    override fun getItemCount() = flatNodeList.size
}

4. 初始化扁平列表

把接口返回的树形数据转换成初始扁平列表(只显示一级节点):

fun convertTreeToFlatList(rootNodes: List<TreeNode>): MutableList<TreeNode> {
    val flatList = mutableListOf<TreeNode>()
    rootNodes.forEach { root ->
        flatList.add(root)
        // 如果需要默认展开某些节点,这里可以递归添加子项
    }
    return flatList
}

方案2:嵌套RecyclerView(兼容原有思路)

如果坚持用嵌套RecyclerView的方式,也可以用单一布局动态配置:

1. 修改列表项布局

在item_tree_node.xml里添加子RecyclerView,默认隐藏:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!-- 父项内容区(和方案1一致) -->
    <LinearLayout
        android:id="@+id/ll_parent_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingVertical="8dp">
        <!-- 缩进、展开按钮、内容文本 -->
    </LinearLayout>

    <!-- 子RecyclerView,默认GONE -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_children"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:layout_marginLeft="24dp"/> <!-- 子项缩进 -->
</LinearLayout>

2. 适配器实现

每个ViewHolder持有子RecyclerView的适配器,动态绑定子项:

class NestedTreeAdapter(private val nodeList: List<TreeNode>) :
    RecyclerView.Adapter<NestedTreeAdapter.NestedTreeViewHolder>() {

    inner class NestedTreeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val parentContentLl: LinearLayout = itemView.findViewById(R.id.ll_parent_content)
        val childrenRv: RecyclerView = itemView.findViewById(R.id.rv_children)
        val expandIv: ImageView = itemView.findViewById(R.id.iv_expand)
        val contentTv: TextView = itemView.findViewById(R.id.tv_content)

        init {
            // 子RecyclerView禁用嵌套滚动,避免滚动冲突
            childrenRv.isNestedScrollingEnabled = false
            childrenRv.layoutManager = LinearLayoutManager(itemView.context)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NestedTreeViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_tree_node, parent, false)
        return NestedTreeViewHolder(view)
    }

    override fun onBindViewHolder(holder: NestedTreeViewHolder, position: Int) {
        val node = nodeList[position]
        holder.contentTv.text = node.content
        holder.expandIv.visibility = if (node.children.isEmpty()) View.GONE else View.VISIBLE
        holder.expandIv.setImageResource(
            if (node.isExpanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more
        )

        // 处理展开/折叠
        holder.expandIv.setOnClickListener {
            node.isExpanded = !node.isExpanded
            holder.childrenRv.visibility = if (node.isExpanded) View.VISIBLE else View.GONE
            if (node.isExpanded) {
                // 给子RecyclerView设置适配器(递归使用同一个适配器)
                holder.childrenRv.adapter = NestedTreeAdapter(node.children)
            }
            notifyItemChanged(position)
        }

        // 初始化时如果节点是展开状态,显示子RecyclerView
        if (node.isExpanded) {
            holder.childrenRv.visibility = View.VISIBLE
            holder.childrenRv.adapter = NestedTreeAdapter(node.children)
        } else {
            holder.childrenRv.visibility = View.GONE
        }
    }

    override fun getItemCount() = nodeList.size
}

注意事项

  • 方案1的扁平列表方式性能更优,避免了多层嵌套RecyclerView的布局开销
  • 如果树形结构层级很深,方案1需要注意递归处理子项的展开/折叠逻辑,避免遗漏
  • 两种方案都可以完全适配接口返回的未知子项数量,不用手动设置多个适配器

内容的提问来源于stack exchange,提问作者Adithya Raj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 21:42:05