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

Android/Java中实现Expandable RecyclerView的最佳方案是什么?

可展开RecyclerView原生实现方案(无第三方依赖)

不用依赖存在兼容问题的第三方ExpandableListView相关库,直接基于原生RecyclerView的多viewType、条目动画能力即可实现稳定的分组展开收起效果,不存在旧库常见的滚动冲突、动画错位、内存泄漏类bug。

核心实现逻辑

将分组条目、子条目统一抽象为同个列表集合中的不同项,通过viewType区分布局类型;为每个分组维护独立的展开状态,点击分组时动态在列表中插入/移除对应子条目,配合RecyclerView自带的条目动画实现流畅的展开/收起过渡。

分步实现

  • 第一步:定义数据结构
    用密封类统一约束列表项类型,分别定义分组、子项的数据结构,在分组项中内置展开状态标记:
    sealed class ExpandableListItem {
        data class Group(
            val id: String,
            val title: String,
            var isExpanded: Boolean = false
        ) : ExpandableListItem()
    
        data class Child(
            val parentGroupId: String,
            val content: String
        ) : ExpandableListItem()
    }
    
  • 第二步:实现多类型Adapter
    内部维护打平后的展示列表,仅在分组展开时将对应子项加入展示集合,收起时移除对应子项:
    class ExpandableRvAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        private val displayList = mutableListOf<ExpandableListItem>()
        private lateinit var groupChildMap: Map<String, List<ExpandableListItem.Child>>
    
        fun submitData(
            groups: List<ExpandableListItem.Group>,
            childMapping: Map<String, List<ExpandableListItem.Child>>
        ) {
            groupChildMap = childMapping
            displayList.clear()
            groups.forEach { group ->
                displayList.add(group)
                if (group.isExpanded) {
                    displayList.addAll(childMapping[group.id] ?: emptyList())
                }
            }
            // 追求更丝滑的刷新效果可以替换为DiffUtil增量更新
            notifyDataSetChanged()
        }
    
        override fun getItemViewType(position: Int): Int {
            return when (displayList[position]) {
                is ExpandableListItem.Group -> VIEW_TYPE_GROUP
                is ExpandableListItem.Child -> VIEW_TYPE_CHILD
            }
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            val inflater = LayoutInflater.from(parent.context)
            return if (viewType == VIEW_TYPE_GROUP) {
                GroupViewHolder(inflater.inflate(R.layout.item_group, parent, false))
            } else {
                ChildViewHolder(inflater.inflate(R.layout.item_child, parent, false))
            }
        }
    
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            when (val item = displayList[position]) {
                is ExpandableListItem.Group -> {
                    (holder as GroupViewHolder).bind(item)
                    holder.itemView.setOnClickListener { toggleGroupExpand(item) }
                }
                is ExpandableListItem.Child -> (holder as ChildViewHolder).bind(item)
            }
        }
    
        private fun toggleGroupExpand(group: ExpandableListItem.Group) {
            val groupIndex = displayList.indexOf(group)
            val childList = groupChildMap[group.id] ?: return
            group.isExpanded = !group.isExpanded
            if (group.isExpanded) {
                // 展开:分组后插入所有子项
                displayList.addAll(groupIndex + 1, childList)
                notifyItemRangeInserted(groupIndex + 1, childList.size)
            } else {
                // 收起:移除分组下所有展示的子项
                repeat(childList.size) { displayList.removeAt(groupIndex + 1) }
                notifyItemRangeRemoved(groupIndex + 1, childList.size)
            }
            // 可在此处触发分组箭头旋转等状态动画
        }
    
        override fun getItemCount() = displayList.size
    
        inner class GroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            private val titleTv = itemView.findViewById<TextView>(R.id.tv_group_title)
            private val arrowIv = itemView.findViewById<ImageView>(R.id.iv_expand_arrow)
            fun bind(group: ExpandableListItem.Group) {
                titleTv.text = group.title
                arrowIv.rotation = if (group.isExpanded) 180f else 0f
            }
        }
    
        inner class ChildViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            private val contentTv = itemView.findViewById<TextView>(R.id.tv_child_content)
            fun bind(child: ExpandableListItem.Child) {
                contentTv.text = child.content
            }
        }
    
        companion object {
            const val VIEW_TYPE_GROUP = 0
            const val VIEW_TYPE_CHILD = 1
        }
    }
    
  • 第三步:初始化列表
    按普通RecyclerView的方式初始化即可,开启默认条目动画就可以获得自然的展开收起效果:
    val rv = findViewById<RecyclerView>(R.id.rv_expandable)
    rv.layoutManager = LinearLayoutManager(this)
    rv.itemAnimator = DefaultItemAnimator()
    val adapter = ExpandableRvAdapter()
    rv.adapter = adapter
    
    // 传入测试数据
    val testGroups = listOf(
        ExpandableListItem.Group("g1", "分组1"),
        ExpandableListItem.Group("g2", "分组2")
    )
    val testChildMap = mapOf(
        "g1" to listOf(
            ExpandableListItem.Child("g1", "子项1-1"),
            ExpandableListItem.Child("g1", "子项1-2")
        ),
        "g2" to listOf(ExpandableListItem.Child("g2", "子项2-1"))
    )
    adapter.submitData(testGroups, testChildMap)
    

方案优势

  • 零第三方依赖,所有逻辑完全可控,不存在废弃库的版本兼容、bug无人修复的问题
  • 完全兼容RecyclerView所有原生特性,支持条目复用、嵌套滚动、局部刷新、分割线、滑动删除等扩展能力
  • 性能优于旧版ExpandableListView,不存在双层滚动布局的焦点冲突问题

布局文件item_group.xml、item_child.xml可以根据业务需求自定义,只需要保证布局id和ViewHolder中绑定的id一致即可。如果需要支持多级展开,只需要在密封类中增加对应层级的条目类型,扩展展开/收起的判断逻辑即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 00:54:31