如何在RecyclerView中添加两级嵌套Section Header(非可展开滚动视图)
没问题,这种两级嵌套Header的RecyclerView实现其实很直观,核心是通过多类型Item来区分一级Header、二级Header和普通内容项,不用任何可展开控件就能实现。我给你一步步拆解实现方案:
1. 先定义清晰的数据结构
首先要把嵌套的旅游数据转换成RecyclerView能直接使用的扁平列表结构,同时用密封类统一所有列表项的类型,方便后续Adapter识别:
// 密封类:统一RecyclerView所有可显示的项类型 sealed class TourListItem { data class TourHeader(val tourName: String) : TourListItem() // 一级Header:旅游名称 data class LocationHeader(val locationName: String) : TourListItem() // 二级Header:地点 data class ContentItem(val content: String) : TourListItem() // 普通内容项:景点/项目 } // 原始层级数据结构(用于构建扁平列表的数据源) data class Tour(val tourName: String, val locationGroups: List<LocationGroup>) data class LocationGroup(val locationName: String, val contents: List<String>)
2. 把嵌套数据转成扁平列表
RecyclerView是线性滚动的,所以我们需要把嵌套的Tour→Location→Content结构,转换成一个包含所有Header和内容的扁平列表:
fun convertToFlatList(tours: List<Tour>): List<TourListItem> { val flatList = mutableListOf<TourListItem>() tours.forEach { tour -> // 先添加当前旅游的一级Header flatList.add(TourListItem.TourHeader(tour.tourName)) // 遍历该旅游下的所有地点,依次添加二级Header和对应内容 tour.locationGroups.forEach { locationGroup -> flatList.add(TourListItem.LocationHeader(locationGroup.locationName)) locationGroup.contents.forEach { content -> flatList.add(TourListItem.ContentItem(content)) } } } return flatList }
3. 实现多类型RecyclerView Adapter
Adapter需要识别不同的Item类型,创建对应的ViewHolder并绑定数据:
class TourAdapter(private val itemList: List<TourListItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { // 定义三种Item类型的常量 companion object { private const val TYPE_TOUR_HEADER = 0 private const val TYPE_LOCATION_HEADER = 1 private const val TYPE_CONTENT = 2 } // 根据位置返回对应的Item类型 override fun getItemViewType(position: Int): Int { return when (itemList[position]) { is TourListItem.TourHeader -> TYPE_TOUR_HEADER is TourListItem.LocationHeader -> TYPE_LOCATION_HEADER is TourListItem.ContentItem -> TYPE_CONTENT } } // 创建对应类型的ViewHolder override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { TYPE_TOUR_HEADER -> { val view = inflater.inflate(R.layout.item_tour_header, parent, false) TourHeaderViewHolder(view) } TYPE_LOCATION_HEADER -> { val view = inflater.inflate(R.layout.item_location_header, parent, false) LocationHeaderViewHolder(view) } else -> { val view = inflater.inflate(R.layout.item_content, parent, false) ContentViewHolder(view) } } } // 绑定对应类型的数据 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (val item = itemList[position]) { is TourListItem.TourHeader -> (holder as TourHeaderViewHolder).bind(item) is TourListItem.LocationHeader -> (holder as LocationHeaderViewHolder).bind(item) is TourListItem.ContentItem -> (holder as ContentViewHolder).bind(item) } } override fun getItemCount() = itemList.size // 一级Header的ViewHolder:可以设置差异化样式(比如加粗、大字体) class TourHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvTourName: TextView = itemView.findViewById(R.id.tv_tour_name) fun bind(item: TourListItem.TourHeader) { tvTourName.text = item.tourName tvTourName.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f) tvTourName.setTypeface(null, Typeface.BOLD) itemView.setBackgroundColor(Color.LTGRAY) itemView.setPadding(16,16,16,16) } } // 二级Header的ViewHolder:样式稍微区分(比如斜体、浅背景) class LocationHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvLocationName: TextView = itemView.findViewById(R.id.tv_location_name) fun bind(item: TourListItem.LocationHeader) { tvLocationName.text = item.locationName tvLocationName.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) tvLocationName.setTypeface(null, Typeface.BOLD_ITALIC) itemView.setBackgroundColor(Color.parseColor("#F5F5F5")) itemView.setPadding(24,12,16,12) } } // 普通内容项的ViewHolder class ContentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvContent: TextView = itemView.findViewById(R.id.tv_content) fun bind(item: TourListItem.ContentItem) { tvContent.text = item.content itemView.setPadding(32,12,16,12) } } }
4. 创建对应的Item布局文件
不需要复杂布局,三个简单的TextView布局即可:
item_tour_header.xml(一级Header):
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_tour_name" android:layout_width="match_parent" android:layout_height="wrap_content"/>
item_location_header.xml(二级Header):
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_location_name" android:layout_width="match_parent" android:layout_height="wrap_content"/>
item_content.xml(普通内容):
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_content" android:layout_width="match_parent" android:layout_height="wrap_content"/>
5. 初始化RecyclerView
在Activity/Fragment中完成数据准备和RecyclerView的初始化:
// 模拟测试数据 val testTours = listOf( Tour( tourName = "欧洲经典之旅", locationGroups = listOf( LocationGroup("法国巴黎", listOf("埃菲尔铁塔", "卢浮宫", "香榭丽舍大街")), LocationGroup("意大利罗马", listOf("斗兽场", "梵蒂冈", "特雷维喷泉")) ) ), Tour( tourName = "东南亚海岛游", locationGroups = listOf( LocationGroup("泰国普吉岛", listOf("芭东海滩", "皮皮岛", "皇帝岛")), LocationGroup("印尼巴厘岛", listOf("乌布皇宫", "情人崖", "蓝梦岛")) ) ) ) // 转换成扁平列表 val flatItemList = convertToFlatList(testTours) // 初始化RecyclerView val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = TourAdapter(flatItemList)
可选优化:固定一级Header在顶部
如果需要滚动时一级Header始终固定在顶部,可以自定义RecyclerView.ItemDecoration实现粘性Header效果,核心是监听滚动位置,绘制固定的Header到RecyclerView的顶部区域。
内容的提问来源于stack exchange,提问作者Omkar Javeri




