如何在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
相关产品推荐
相关产品推荐

