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

如何在Android的RecyclerView中实现多级可展开/折叠列表项

实现Android RecyclerView多级展开折叠列表的方案

不推荐使用嵌套RecyclerView实现,会导致严重的性能问题、滑动流畅度差,最优方案是将所有层级的条目平铺为一维列表,通过控制显示列表的增删实现展开折叠。

第一步:定义通用条目数据结构

先封装所有层级条目的公共属性,支持扩展任意业务数据:

// 通用多级列表条目基类
open class MultiLevelItem<T> {
    // 条目层级,0=主项,1=一级子项,2=二级子项,可扩展更多层级
    var level: Int = 0
    // 当前是否处于展开状态
    var isExpanded: Boolean = false
    // 是否支持展开(无子条目的条目不需要开启)
    var isExpandable: Boolean = false
    // 子条目列表
    var children: MutableList<MultiLevelItem<T>>? = null
    // 持有的实际业务数据
    var data: T? = null
}

第二步:实现多类型Adapter

用RecyclerView原生多类型能力适配不同层级的布局,核心逻辑为展开折叠时动态增删显示列表的条目:

class MultiLevelAdapter<T>(private val allItems: MutableList<MultiLevelItem<T>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    // 实际渲染的显示列表,初始仅加载所有主项
    private val showItems = mutableListOf<MultiLevelItem<T>>().apply {
        addAll(allItems.filter { it.level == 0 })
    }

    companion object {
        const val TYPE_LEVEL_0 = 0 // 主项视图类型
        const val TYPE_LEVEL_1 = 1 // 一级子项视图类型
        const val TYPE_LEVEL_2 = 2 // 二级子项视图类型
    }

    override fun getItemViewType(position: Int): Int {
        return showItems[position].level
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when(viewType) {
            TYPE_LEVEL_0 -> Level0ViewHolder(inflater.inflate(R.layout.item_level_0, parent, false))
            TYPE_LEVEL_1 -> Level1ViewHolder(inflater.inflate(R.layout.item_level_1, parent, false))
            TYPE_LEVEL_2 -> Level2ViewHolder(inflater.inflate(R.layout.item_level_2, parent, false))
            else -> throw IllegalArgumentException("未知条目类型")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = showItems[position]
        when(holder) {
            is Level0ViewHolder -> {
                holder.tvTitle.text = item.data?.toString()
                // 切换展开图标状态
                holder.ivExpand.rotation = if(item.isExpanded) 90f else 0f
                // 点击触发展开折叠
                holder.itemView.setOnClickListener { toggleExpand(item, position) }
                // 按层级设置左边距模拟缩进,每级可自定义缩进值
                holder.itemView.setPadding(dp2px(20), 0, 0, 0)
            }
            is Level1ViewHolder -> {
                holder.tvTitle.text = item.data?.toString()
                holder.ivExpand.visibility = if(item.isExpandable) View.VISIBLE else View.GONE
                if (item.isExpandable) {
                    holder.ivExpand.rotation = if(item.isExpanded) 90f else 0f
                    holder.itemView.setOnClickListener { toggleExpand(item, position) }
                }
                holder.itemView.setPadding(dp2px(40), 0, 0, 0)
            }
            is Level2ViewHolder -> {
                holder.tvTitle.text = item.data?.toString()
                holder.itemView.setPadding(dp2px(60), 0, 0, 0)
            }
        }
    }

    override fun getItemCount(): Int = showItems.size

    // 核心展开折叠逻辑
    private fun toggleExpand(item: MultiLevelItem<T>, position: Int) {
        if (!item.isExpandable || item.children.isNullOrEmpty()) return
        item.isExpanded = !item.isExpanded
        if (item.isExpanded) {
            // 展开:将子条目插入当前条目下方
            item.children?.let { children ->
                showItems.addAll(position + 1, children)
                notifyItemRangeInserted(position + 1, children.size)
            }
            notifyItemChanged(position)
        } else {
            // 折叠:递归移除所有后代条目
            item.children?.let { children ->
                var removeCount = 0
                fun countRemoveItems(list: List<MultiLevelItem<T>>) {
                    list.forEach { child ->
                        removeCount++
                        if (child.isExpanded && child.children != null) {
                            countRemoveItems(child.children!!)
                        }
                    }
                }
                countRemoveItems(children)
                showItems.subList(position + 1, position + 1 + removeCount).clear()
                notifyItemRangeRemoved(position + 1, removeCount)
            }
            notifyItemChanged(position)
        }
    }

    // 工具方法:dp转px
    private fun dp2px(dp: Int): Int {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics).toInt()
    }

    // 各层级ViewHolder定义,布局可按业务自定义
    class Level0ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvTitle = itemView.findViewById<TextView>(R.id.tv_title)
        val ivExpand = itemView.findViewById<ImageView>(R.id.iv_expand)
    }
    class Level1ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvTitle = itemView.findViewById<TextView>(R.id.tv_title)
        val ivExpand = itemView.findViewById<ImageView>(R.id.iv_expand)
    }
    class Level2ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvTitle = itemView.findViewById<TextView>(R.id.tv_title)
    }
}

第三步:组装数据调用

按你的需求构造对应层级的测试数据,绑定到RecyclerView即可:

// 构造符合需求的测试数据
fun buildTestData(): MutableList<MultiLevelItem<String>> {
    val allItems = mutableListOf<MultiLevelItem<String>>()

    // 主项1
    val main1 = MultiLevelItem<String>().apply {
        level = 0
        isExpandable = true
        data = "主项1"
        children = mutableListOf()
    }
    val main1Child1 = MultiLevelItem<String>().apply {
        level = 1
        isExpandable = true
        data = "子项1"
        children = mutableListOf(
            MultiLevelItem<String>().apply { level = 2; data = "二级子项1" },
            MultiLevelItem<String>().apply { level = 2; data = "二级子项2" }
        )
    }
    val main1Child2 = MultiLevelItem<String>().apply {
        level = 1
        isExpandable = true
        data = "子项2"
        children = mutableListOf(
            MultiLevelItem<String>().apply { level = 2; data = "二级子项1" }
        )
    }
    main1.children?.addAll(listOf(main1Child1, main1Child2))

    // 主项2
    val main2 = MultiLevelItem<String>().apply {
        level = 0
        isExpandable = true
        data = "主项2"
        children = mutableListOf()
    }
    val main2Child1 = MultiLevelItem<String>().apply {
        level = 1
        isExpandable = true
        data = "子项1"
        children = mutableListOf(
            MultiLevelItem<String>().apply { level = 2; data = "二级子项1" },
            MultiLevelItem<String>().apply { level = 2; data = "二级子项2" }
        )
    }
    val main2Child2 = MultiLevelItem<String>().apply { level = 1; data = "子项2" }
    val main2Child3 = MultiLevelItem<String>().apply { level = 1; data = "子项3" }
    main2.children?.addAll(listOf(main2Child1, main2Child2, main2Child3))

    // 主项3、主项4(不可展开)
    val main3 = MultiLevelItem<String>().apply { level = 0; data = "主项3" }
    val main4 = MultiLevelItem<String>().apply { level = 0; data = "主项4" }

    allItems.addAll(listOf(main1, main2, main3, main4))
    return allItems
}

// 页面中绑定RecyclerView
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MultiLevelAdapter(buildTestData())

可选优化点

  • 需要支持更多层级时,仅新增对应ViewHolder类型即可,核心逻辑无需修改
  • 可给展开图标增加属性动画,实现旋转过渡效果
  • 需要默认展开指定条目时,初始化showItems时加入对应子条目,同时设置isExpanded = true即可
  • 条目选中状态可在MultiLevelItem中新增isSelected字段,绑定时做对应样式切换即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 13:57:03