Android UI 组件系列(十二):RecyclerView 嵌套及点击事件
博客专栏:Android初级入门UI组件与布局
源码:通过网盘分享的文件:Android入门布局及UI相关案例
链接: https://pan.baidu.com/s/1EOuDUKJndMISolieFSvXXg?pwd=4k9n 提取码: 4k9n
引言
在前两篇中,我们已经深入掌握了 RecyclerView 的基础使用方法,以及如何通过多类型 ViewHolder 实现复杂布局和数据刷新。这些能力,已经可以覆盖 80% 的列表需求。
但当页面结构进一步复杂,比如首页常见的「顶部横向 Banner + 底部瀑布流列表」,或者内容卡片中又嵌套横向滑动项时,普通的多类型布局已经难以胜任。这时候,我们就需要掌握 RecyclerView 的高级技能之一——嵌套 RecyclerView。
此外,在列表中添加“收藏”“分享”这类操作按钮时,事件传递逻辑也变得更加复杂。如果事件处理方式混乱,极易导致点击无响应、状态不同步等问题。
因此,本篇作为 RecyclerView 实战系列的第 3 篇,将聚焦两个关键点:
- ✅ 嵌套 RecyclerView 的实现方式与注意事项(例如横向列表嵌套、SpanSize 跨度控制等);
- ✅ 列表项与子控件的点击事件设计与传递(包括 Adapter 到 Activity 的回调封装)。
通过一个实际的 UI Demo,我们将手把手实现如下结构:
[横向 Banner 区域] —— RecyclerView 横向嵌套
[推荐直播] 标题 —— TextView
[直播卡片1] [直播卡片2]
[直播卡片3] [直播卡片4]
...
每个直播卡片都支持点击整卡 & 收藏按钮。所有点击事件都将通过清晰的接口方式传回到 Activity 层,确保代码解耦、职责明确。
一、嵌套 RecyclerView 的常见场景与实现方式
1.1 为什么要用嵌套 RecyclerView?
在实际项目中,有很多页面需要展示不同方向或不同布局的列表组合,例如:
- 首页顶部的 横向 Banner
- 分类页中的 横向子频道
- 内容详情页中嵌套的 相关推荐 区域
这些结构具有一个共同点:
需要在纵向列表中嵌套横向列表
如果强行使用一个 RecyclerView 来“拼接出横向滑动效果”,不仅实现复杂,复用性差,还容易出错。而 RecyclerView 支持子项再嵌套 RecyclerView,恰好就是官方推荐的做法。
1.2 Demo 页面结构说明
在本篇的实战中,我们设计了如下页面结构:
纵向主 RecyclerView(GridLayoutManager)
├── item 0: 横向 Banner(嵌套 RecyclerView)
├── item 1: 标题项(TextView)
├── item 2-N: 直播项(每行两个,瀑布流效果)
1.3 技术实现要点
✅ 1. 外层使用 GridLayoutManager 实现瀑布流布局
我们希望直播卡片每行两个,而 Banner 与标题各占一整行,所以采用:
//3.设置LayoutManagerval layoutManager = GridLayoutManager(this, 2)layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {override fun getSpanSize(position: Int): Int {return when (mainAdapter.getItemViewType(position)) {MainAdapter.TYPE_LIVE -> 1else -> 2}}}
✅ 2. 横向 Banner 区域使用嵌套 RecyclerView
Banner 区域的布局本身就是一个 item,内部包含一个横向 RecyclerView:
<androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/bannerRecyclerView"android:layout_width="match_parent"android:layout_height="180dp"android:orientation="horizontal"android:clipToPadding="false" />
Adapter 中正常为其设置横向 LinearLayoutManager 和数据源即可。
inner class BannerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {private val bannerRecyclerView: RecyclerView = itemView.findViewById(R.id.bannerRecyclerView)fun bind() {bannerRecyclerView.layoutManager =LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false)val bannerItems = listOf(BannerItem(R.drawable.placeholder_banner),BannerItem(R.drawable.placeholder_banner))bannerRecyclerView.adapter = BannerAdapter(bannerItems) { item,index ->Toast.makeText(itemView.context,"点击了 Banner 第 ${index + 1} 张图:${item.imageResId}",Toast.LENGTH_SHORT).show()}}}
1.4 实战实现:一步步搭建嵌套 RecyclerView
整个实现流程分为四步:
✅ 第一步:布局文件准备(XML)
我们需要如下布局文件来支撑不同类型的 item:
文件名 | 用途 |
---|---|
activity_main.xml | 主界面,包含主 RecyclerView |
item_banner.xml | 横向嵌套 RecyclerView 区域 |
item_banner_image.xml | 横向列表中每个 Banner 图 |
item_title.xml | 列表中的标题项 |
item_live.xml | 每个直播项(卡片 + 收藏按钮) |
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/mainRecyclerView"android:layout_width="match_parent"android:layout_height="match_parent"android:padding="8dp"android:clipToPadding="false" />
item_banner.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/bannerRecyclerView"android:layout_width="match_parent"android:layout_height="180dp"android:orientation="horizontal"android:paddingStart="8dp"android:paddingEnd="8dp"android:clipToPadding="false" />
item_banner_image.xml:
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/bannerImage"android:layout_width="300dp"android:layout_height="match_parent"android:scaleType="fitCenter"android:layout_marginEnd="12dp"android:background="#DDD" />
item_title.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/titleText"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="推荐直播"android:textSize="18sp"android:textStyle="bold"android:padding="12dp"android:textColor="#222" />
item_live.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:layout_margin="6dp"android:background="#FFF"android:elevation="2dp"android:padding="6dp"><ImageViewandroid:id="@+id/coverImage"android:layout_width="match_parent"android:layout_height="180dp"android:scaleType="centerCrop"android:background="#CCC" /><TextViewandroid:id="@+id/liveTitle"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="直播标题"android:textSize="14sp"android:textColor="#333"android:paddingTop="6dp"android:maxLines="2" /><Buttonandroid:id="@+id/btnFavorite"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="收藏"android:layout_marginTop="4dp"android:textSize="12sp"/>
</LinearLayout>
✅ 第二步:数据模型设计(Model)
在多类型列表中,合理的数据模型设计可以大大简化后续的 Adapter 编写与类型判断逻辑。本项目中,我们将页面中的三类内容抽象成三个模型对象:
📌 1. BannerItem —— 横向 Banner 区域的图片模型
data class BannerItem(val imageResId: Int // 使用本地资源 ID 显示图片
)
📌 2. LiveItem —— 直播列表的单个卡片模型
data class LiveItem(val coverResId: Int, // 封面图资源val title: String, // 标题val isFavorite: Boolean // 是否收藏
)
📌 3. MainListItem —— 用于主 RecyclerView 的多类型封装
sealed class MainListItem {object Banner : MainListItem()data class Title(val text: String) : MainListItem()data class Live(val liveItem: LiveItem) : MainListItem()
}
MainListItem 使用 Kotlin 的 sealed class 特性,将不同类型的 item 统一封装,后续在 Adapter 中可以通过 when(item) 结构轻松判断当前 item 类型。
✅ 第三步:Adapter 与 ViewHolder 实现
通过合理划分 ViewType、使用 sealed class 管理数据模型,我们可以非常清晰地实现一个嵌套 + 多类型列表。
横向 Banner 区域的 BannerAdapter
BannerAdapter 是一个标准的 RecyclerView Adapter,用于渲染横向滚动的图片项。
class BannerAdapter(private val data: List<BannerItem>,private val onClick: (BannerItem, Int) -> Unit
) : RecyclerView.Adapter<BannerAdapter.BannerViewHolder>() {...
}
主列表 Adapter:MainAdapter
MainAdapter 是整个页面最关键的部分,它支持三种类型的 ViewHolder:
- BannerHolder:嵌套横向 RecyclerView
- TitleHolder:分组标题项
- LiveHolder:直播卡片(带收藏按钮)
通过 getItemViewType() 方法,我们将 MainListItem 映射为不同的布局类型:
override fun getItemViewType(position: Int): Int {return when (items[position]) {is MainListItem.Banner -> TYPE_BANNERis MainListItem.Title -> TYPE_TITLEis MainListItem.Live -> TYPE_LIVE}
}
并在 onCreateViewHolder() 中使用 LayoutInflater 加载对应的布局:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {return when (viewType) {TYPE_BANNER -> BannerHolder(...)TYPE_TITLE -> TitleHolder(...)TYPE_LIVE -> LiveHolder(...)else -> ...}
}
BannerHolder:实现嵌套横向 RecyclerView
inner class BannerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {fun bind() {bannerRecyclerView.layoutManager =LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false)...}
}
TitleHolder:展示分组标题
inner class TitleHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {fun bind(title: MainListItem.Title) {itemView.findViewById<TextView>(R.id.titleText).text = title.text}
}
LiveHolder:展示直播卡片 + 收藏按钮点击事件
inner class LiveHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {fun bind(item: LiveItem) {...btn.setOnClickListener { onFavoriteClick(item) }itemView.setOnClickListener { onLiveClick(item) }}
}
✅ 第四步:Activity 页面搭建与嵌套列表初始化
完成 Adapter 之后,我们就可以在 MainActivity 中将所有内容串联起来,真正跑起来嵌套 RecyclerView 的整体布局。
MainActivity 中的核心初始化逻辑集中在一个独立的 setupRecyclerView() 方法中,包括:
- 构建数据源;
- 初始化 Adapter;
- 设置 LayoutManager;
- 将 Adapter 应用于 RecyclerView。
构建数据源
val list = mutableListOf<MainListItem>()
list.add(MainListItem.Banner)
list.add(MainListItem.Title("推荐直播"))val liveList = listOf(LiveItem(R.drawable.placeholder_live, "小王正在唱歌", false),LiveItem(R.drawable.placeholder_live, "一起看电影", true),LiveItem(R.drawable.placeholder_live, "画画直播", false),LiveItem(R.drawable.placeholder_live, "夜宵时间", false)
)
liveList.forEach {list.add(MainListItem.Live(it))
}
设置 GridLayoutManager 实现混排样式
因为直播卡片要每行两个,而 Banner 和标题要单独占一行,所以我们使用了 GridLayoutManager 并结合 spanSizeLookup 精准控制每种 item 的占列数量:
val layoutManager = GridLayoutManager(this, 2)
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {override fun getSpanSize(position: Int): Int {return when (mainAdapter.getItemViewType(position)) {MainAdapter.TYPE_LIVE -> 1 // 直播项每行两个else -> 2 // 其他项占满整行}}
}
应用 Adapter 和 LayoutManager
mainRecyclerView.layoutManager = layoutManager
mainRecyclerView.adapter = mainAdapter
二、点击事件的实现与事件传递
在列表开发中,点击事件的处理往往被低估了它的复杂度。一旦你的 UI 中既有整卡点击,又有子控件点击(比如收藏按钮),再加上 Adapter 和 ViewHolder 分离、数据结构复杂等问题,事件传递就极易变得混乱。
本节我们将通过实际代码,分步骤讲解点击事件从 ViewHolder → Adapter → Activity 的完整传递流程,并给出一套清晰的实现模式。
2.1 点击事件的需求分析
我们要实现的点击交互包括:
点击位置 | 事件行为 |
---|---|
直播卡片整体 | 跳转或提示直播信息 |
直播卡片中的收藏按钮 | 切换收藏状态(或提示) |
Banner 图片 | 弹出点击了第几张 Banner |
2.2 事件处理的基本思路
为了让事件能从 ViewHolder 被传递到外层的 Activity,我们采用“回调函数”的方式,让 Adapter 接收一个 lambda 参数,负责将点击事件抛给外部处理。
class MainAdapter(private val items: List<MainListItem>,private val onLiveClick: (LiveItem) -> Unit,private val onFavoriteClick: (LiveItem) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {...
}
通过这种方式,我们在 Activity 中传入事件处理逻辑,而 Adapter 和 ViewHolder 仅负责绑定和回调,不关心点击之后要干什么,从而达到了职责分离。
2.3 Live 项点击事件实现
✅ ViewHolder 内部设置点击回调
inner class LiveHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {fun bind(item: LiveItem) {...btn.setOnClickListener { onFavoriteClick(item) }itemView.setOnClickListener { onLiveClick(item) }}
}
这样一来:
- 用户点击整卡时,调用 onLiveClick(item)
- 用户点击“收藏”按钮时,调用 onFavoriteClick(item)
✅ Adapter 构造时传入事件处理逻辑
mainAdapter = MainAdapter(items = list,onLiveClick = { item ->Toast.makeText(this, "点击直播:${item.title}", Toast.LENGTH_SHORT).show()},onFavoriteClick = { item ->Toast.makeText(this, "点击收藏:${item.title}", Toast.LENGTH_SHORT).show()}
)
2.4 Banner 区域点击事件实现
Banner 属于嵌套 RecyclerView 的 item,我们在 BannerAdapter 中使用类似方式处理点击:
class BannerAdapter(private val data: List<BannerItem>,private val onClick: (BannerItem, Int) -> Unit
)
✅ BannerAdapter 的 onBindViewHolder:
override fun onBindViewHolder(holder: BannerViewHolder, position: Int) {val item = data[position]holder.imageView.setImageResource(item.imageResId)holder.itemView.setOnClickListener {onClick(item, position)}
}
✅ 在 BannerHolder 中使用:
bannerRecyclerView.adapter = BannerAdapter(bannerItems) { item, index ->Toast.makeText(itemView.context,"点击了 Banner 第 ${index + 1} 张图",Toast.LENGTH_SHORT).show()
}
结语
RecyclerView 是 Android 中最灵活也最常用的 UI 组件之一,而嵌套列表与点击事件正是它“进阶玩法”的核心。本篇我们通过一个完整的实战 Demo,从页面布局、数据模型、Adapter 多类型处理,到 RecyclerView 的嵌套实现与点击事件回调,逐步构建出一个具备真实场景意义的页面结构。
通过这个案例,你应该已经掌握:
- 如何在 RecyclerView 中优雅地嵌套另一个 RecyclerView(如横向 Banner);
- 如何利用 GridLayoutManager 的 spanSizeLookup 实现复杂的布局混排;
- 如何通过 lambda 回调的方式,从 ViewHolder 将点击事件向上传递至 Activity 层;
- 如何设计结构清晰、可扩展的数据模型与多类型 Adapter。
在实际项目中,这类嵌套 + 多事件交互的页面非常常见,比如内容首页、电商推荐页、发现页等,掌握这一套设计模式后,你可以更从容地应对各类复杂列表页面的挑战。
至此,RecyclerView 实战系列的第三篇就结束啦。如果你还没读前两篇,也欢迎回顾:
-
第一篇:RecyclerView 的基础使用
-
第二篇:RecyclerView 多类型布局与数据刷新实战