You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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

火山引擎 最新活动