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

