前言
在 Android 开发中,列表展示是最核心、最常用的 UI 场景之一。从早期的 ListView 到如今的 RecyclerView,列表控件的迭代不仅带来了性能的飞跃,更让复杂多样的列表布局实现变得更加灵活。
本文将以《Android 移动开发基础案例教程(第 3 版)》中的仿今日头条推荐列表项目为蓝本,用 2 万字的深度解析,带你彻底搞懂 RecyclerView 的核心原理、使用流程、布局设计、控件搭配,以及多类型列表的完整实现方案。无论你是刚入门的 Android 新手,还是想系统梳理 RecyclerView 知识的开发者,这篇博客都能帮你从 0 到 1 吃透这个核心控件。
一、项目背景与核心需求拆解
1.1 项目效果与目标
本项目最终要实现一个仿今日头条的推荐列表界面,核心效果如下:
-
顶部有自定义标题栏,包含搜索框和导航标签
-
列表支持3 种条目样式:
- 无图 / 置顶样式(仅文字,无图片)
- 单图样式(标题 + 文字 + 1 张图片)
- 三图样式(标题 + 文字 + 3 张横向排列的图片)
-
列表滚动流畅,条目复用高效,样式统一可维护
1.2 为什么选择 RecyclerView 而非 ListView
很多新手会疑惑:为什么不用 ListView 实现?这里先做一个核心对比,帮你理解技术选型的原因:
表格
| 特性 | ListView | RecyclerView |
|---|---|---|
| 视图复用 | 仅复用 convertView,逻辑需手动实现,易出错 | 内置 ViewHolder 模式,强制复用,性能更优 |
| 布局管理器 | 仅支持垂直列表,横向需自定义 | 支持线性、网格、瀑布流,可自定义布局管理器 |
| 条目动画 | 无原生支持,需手动实现 | 内置 ItemAnimator,支持增删改查动画 |
| 多类型条目 | 需手动在 getView 中判断类型,逻辑复杂 | 原生支持 getItemViewType,多类型实现更优雅 |
| 性能优化 | 无预取、局部刷新等优化 | 支持预取、局部刷新、差分更新等高级优化 |
本项目需要实现多类型条目、高效滚动,因此 RecyclerView 是唯一的最优选择。
1.3 项目整体架构梳理
在开始代码解析前,我们先梳理整个项目的核心模块,让你对整体流程有清晰认知:
-
项目创建与依赖引入:创建工程,引入
androidx.recyclerview库 -
样式与资源准备:定义文本样式、图片样式、颜色值,统一 UI 风格
-
布局文件设计:
- 主布局
activity_main.xml:承载标题栏、分割线、RecyclerView - 标题栏布局
title_bar.xml:复用的顶部导航栏 - 单图条目布局
list_item_one.xml:单图 / 无图样式 - 三图条目布局
list_item_two.xml:三图样式
- 主布局
-
数据实体类:
NewsBean.java,封装新闻数据 -
适配器实现:
NewsAdapter.java,核心的 RecyclerView 适配器,处理多类型条目 -
主 Activity 逻辑:
MainActivity.java,初始化数据、绑定 RecyclerView、设置布局管理器 -
样式与主题配置:
styles.xml、colors.xml、AndroidManifest.xml,统一主题与样式
二、RecyclerView 核心基础:原理与核心组件
在进入项目代码前,我们必须先吃透 RecyclerView 的核心原理,否则只会 “抄代码” 而不懂原理,遇到问题就会无从下手。
2.1 RecyclerView 核心原理
RecyclerView 是一个 **“容器型控件” ,它本身不负责条目布局、条目样式,只负责复用视图、回收视图、滚动布局 **,所有的条目逻辑都交给适配器 Adapter 处理。
它的核心工作流程是:
- 测量布局:布局管理器
LayoutManager负责计算每个条目的位置和大小 - 视图创建:适配器
onCreateViewHolder创建条目视图,封装为ViewHolder - 数据绑定:适配器
onBindViewHolder给条目视图绑定数据 - 视图复用:当条目滑出屏幕时,RecyclerView 回收
ViewHolder;新条目滑入时,复用回收的ViewHolder,只重新绑定数据,避免重复创建视图,大幅提升性能 - 视图回收:系统自动回收不可见的条目,优化内存
2.2 RecyclerView 四大核心组件
2.2.1 RecyclerView 控件本身
RecyclerView 是列表的容器,在布局中声明后,需要:
- 设置布局管理器(决定列表的排列方式)
- 设置适配器(决定条目样式和数据绑定)
- 可选设置条目动画、分割线等
2.2.2 LayoutManager(布局管理器)
LayoutManager 是 RecyclerView 的 “布局大脑”,负责:
- 测量和摆放所有条目
- 处理滚动逻辑
- 决定条目复用的时机
Android 提供了 3 种原生布局管理器:
- LinearLayoutManager:线性布局,支持垂直 / 水平列表(本项目使用)
- GridLayoutManager:网格布局,支持多列列表
- StaggeredGridLayoutManager:瀑布流布局,支持不规则高度的多列列表
2.2.3 Adapter(适配器)
Adapter 是 RecyclerView 的 “数据桥梁”,是我们开发中最核心的部分,负责:
- 创建 ViewHolder(
onCreateViewHolder) - 绑定数据到 ViewHolder(
onBindViewHolder) - 返回条目总数(
getItemCount) - 多类型条目时,返回条目类型(
getItemViewType)
2.2.4 ViewHolder(视图持有者)
ViewHolder 是条目的 “视图缓存容器”,它的核心作用是缓存条目内的所有控件,避免每次绑定数据时都调用 findViewById,大幅提升性能。
在本项目中,因为有两种条目样式,所以我们定义了两个 ViewHolder:MyViewViewHolder1(单图 / 无图)和 MyViewViewHolder2(三图)。
三、项目从零搭建:从创建到样式配置
3.1 项目创建与依赖引入
3.1.1 创建项目
按照教程要求,创建名为 HeadLine 的应用,包名 cn.itcast.headline,选择 Empty Activity 模板,语言选择 Java。
3.1.2 引入 RecyclerView 依赖
RecyclerView 属于 AndroidX 库,需要在 app/build.gradle(Module 级别)的 dependencies 中添加依赖:
gradle
dependencies {
// 其他依赖...
implementation 'androidx.recyclerview:recyclerview:1.3.2'
}
添加后同步 Gradle,即可在项目中使用 RecyclerView 控件。
3.1.3 导入界面图片
将项目需要的图片 food.png、e_sports.png、fruit1.png、fruit2.png、fruit3.png、search_bg.png、sleep1.png、sleep2.png、sleep3.png、takeout.png、top.png 导入到 res/drawable-hdpi 文件夹中,用于条目图片展示。
3.2 样式与颜色配置:统一 UI 风格
为了让列表样式统一、代码可维护,我们将重复的样式抽取到 styles.xml,颜色值抽取到 colors.xml,避免在布局中重复写相同属性。
3.2.1 定义文本样式(styles.xml)
在 res/values/styles.xml 中定义两种文本样式:tvStyle(导航栏标题样式)和 tvInfo(条目底部文字样式),代码如下:
xml
<!-- 导航栏文本样式 -->
<style name="tvStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">match_parent</item>
<item name="android:padding">10dp</item>
<item name="android:gravity">center</item>
<item name="android:textSize">15sp</item>
</style>
<!-- 条目底部文字样式 -->
<style name="tvInfo">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">@color/gray_color</item>
</style>
tvStyle:用于顶部导航栏的 “推荐”“AI”“小视频” 等标签,设置居中、内边距、字号tvInfo:用于条目底部的用户名、评论数、时间,设置左间距、垂直居中、深灰色文字
3.2.2 定义图片样式(styles.xml)
条目内的图片样式统一,因此定义 ivImg 样式,代码如下:
xml
<style name="ivImg">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<!-- ll_info 为布局文件 list_item_one.xml 中的 id -->
<item name="android:layout_toRightOf">@id/ll_info</item>
</style>
layout_weight="1":在横向线性布局中平分宽度,实现 3 张图片等宽layout_toRightOf:让图片在文字布局的右侧,实现左文右图的布局
3.2.3 定义颜色值(colors.xml)
在 res/values/colors.xml 中定义背景色和文字色,方便全局调用:
xml
<color name="light_gray_color">#eeeeee</color>
<color name="gray_color">#828282</color>
light_gray_color:列表背景浅灰色gray_color:条目底部文字深灰色
3.2.4 删除默认标题栏
Android 默认的 ActionBar 样式不符合需求,需要在 AndroidManifest.xml 中修改主题:
xml
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBarBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
将主题改为 Theme.AppCompat.NoActionBar,即可隐藏默认标题栏,使用自定义标题栏。
四、布局文件全解析:从主布局到条目布局
布局是 RecyclerView 的 “骨架”,本项目包含 4 个核心布局文件,我们逐个拆解每个布局的作用、控件、属性和使用逻辑。
4.1 标题栏布局:title_bar.xml
4.1.1 布局作用
将顶部标题栏抽取为独立布局,通过 <include> 标签引入主布局,实现代码复用,避免主布局代码冗余。
4.1.2 布局结构与控件
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:background="@color/light_gray_color"
android:orientation="horizontal">
<!-- 仿今日头条文本 -->
<TextView
style="@style/tvStyle"
android:text="仿今日头条"
android:textColor="#000000"
android:textSize="18sp" />
<!-- 搜索输入框 -->
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="5dp"
android:background="@drawable/search_bg"
android:hint="搜索"
android:padding="5dp" />
<!-- 导航标签:推荐 -->
<TextView
style="@style/tvStyle"
android:text="推荐"
android:textColor="#FF5722" />
<!-- 导航标签:AI -->
<TextView
style="@style/tvStyle"
android:text="AI"
android:textColor="#000000" />
<!-- 导航标签:小视频 -->
<TextView
style="@style/tvStyle"
android:text="小视频"
android:textColor="#000000" />
</LinearLayout>
4.1.3 核心控件与属性解析
-
根布局:LinearLayout(横向)
orientation="horizontal":横向排列子控件background="@color/light_gray_color":设置浅灰色背景layout_width="match_parent"、layout_height="wrap_content":宽度铺满屏幕,高度自适应内容
-
TextView(仿今日头条)
- 引用
@style/tvStyle统一样式 textSize="18sp":放大字号,突出标题textColor="#000000":黑色文字
- 引用
-
EditText(搜索框)
layout_weight="1":在横向布局中占据剩余空间,实现宽度自适应background="@drawable/search_bg":设置自定义搜索框背景hint="搜索":占位提示文字
-
导航标签 TextView
- 全部引用
@style/tvStyle,保证样式统一 - “推荐” 标签设置橙色文字
#FF5722,突出当前选中状态
- 全部引用
4.2 主布局:activity_main.xml
4.2.1 布局作用
作为 Activity 的主布局,承载标题栏、分割线、RecyclerView,是整个列表的根布局。
4.2.2 布局结构与控件
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="match_parent"
android:background="@color/light_gray_color"
android:orientation="vertical">
<!-- 引入自定义标题栏 -->
<include layout="@layout/title_bar" />
<!-- 灰色分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/gray_color" />
<!-- RecyclerView 列表容器 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray_color" />
</LinearLayout>
4.2.3 核心控件与属性解析
-
根布局:LinearLayout(垂直)
orientation="vertical":垂直排列子控件background="@color/light_gray_color":全局浅灰色背景
-
标签
- 引入
title_bar.xml布局,实现标题栏复用,代码简洁易维护
- 引入
-
View(分割线)
- 用一个高度 1dp 的 View 实现分割线,比使用 Drawable 更简单
background="@color/gray_color":深灰色,与条目文字颜色统一
-
RecyclerView 控件
id="@+id/rv_list":在 Activity 中通过findViewById找到该控件layout_width="match_parent"、layout_height="match_parent":铺满剩余屏幕空间- 注意:必须使用完整类名
androidx.recyclerview.widget.RecyclerView,否则会报错
4.3 单图 / 无图条目布局:list_item_one.xml
4.3.1 布局作用
用于展示无图(置顶)和单图两种条目样式,通过代码控制图片的显示 / 隐藏,实现一个布局适配两种样式。
4.3.2 布局结构与控件
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:layout_margin="5dp"
android:background="@drawable/item_bg"
android:orientation="horizontal"
android:padding="5dp">
<!-- 左侧文字布局 -->
<LinearLayout
android:id="@+id/ll_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<!-- 新闻标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#000000"
android:maxLines="2"
android:ellipsize="end" />
<!-- 底部信息布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal">
<!-- 用户名 -->
<TextView
android:id="@+id/tv_name"
style="@style/tvInfo"
android:text="央视新闻客户端" />
<!-- 评论数 -->
<TextView
android:id="@+id/tv_comment"
style="@style/tvInfo"
android:text="9884评" />
<!-- 发布时间 -->
<TextView
android:id="@+id/tv_time"
style="@style/tvInfo"
android:text="6小时前" />
</LinearLayout>
</LinearLayout>
<!-- 置顶图标 -->
<ImageView
android:id="@+id/iv_top"
style="@style/ivImg"
android:src="@drawable/top"
android:layout_marginLeft="5dp" />
<!-- 新闻图片 -->
<ImageView
android:id="@+id/iv_img"
style="@style/ivImg"
android:layout_marginLeft="5dp" />
</LinearLayout>
4.3.3 核心控件与属性解析
-
根布局:LinearLayout(横向)
orientation="horizontal":左文右图布局layout_margin="5dp"、padding="5dp":设置条目内边距和外边距,避免内容贴边background="@drawable/item_bg":设置条目白色背景,与浅灰色列表背景区分
-
左侧文字布局(ll_info)
layout_weight="1":占据横向布局的剩余空间,图片布局在右侧orientation="vertical":垂直排列标题和底部信息id="@+id/ll_info":用于图片样式的layout_toRightOf属性,实现左文右图
-
新闻标题 TextView(tv_title)
maxLines="2":限制最多显示 2 行,避免标题过长ellipsize="end":超出部分用省略号结尾,保证布局美观textSize="16sp":标题字号大于底部文字,突出层级
-
底部信息布局
- 横向排列用户名、评论数、时间,全部引用
@style/tvInfo统一样式 layout_marginTop="5dp":与标题保持间距
- 横向排列用户名、评论数、时间,全部引用
-
ImageView(iv_top、iv_img)
- 引用
@style/ivImg统一样式 iv_top:置顶图标,默认显示,第一条目(置顶)显示,其他条目隐藏iv_img:新闻图片,无图条目隐藏,单图条目显示- 通过代码控制两个 ImageView 的
visibility,实现一个布局适配两种样式
- 引用
4.4 三图条目布局:list_item_two.xml
4.4.1 布局作用
用于展示3 张图片的条目样式,标题在上,3 张图片横向等宽排列,底部是用户信息。
4.4.2 布局结构与控件
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:layout_margin="5dp"
android:background="@drawable/item_bg"
android:orientation="vertical"
android:padding="5dp">
<!-- 新闻标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#000000"
android:maxLines="2"
android:ellipsize="end" />
<!-- 三张图片布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal">
<!-- 图片1 -->
<ImageView
android:id="@+id/iv_img1"
style="@style/ivImg"
android:layout_margin="2dp" />
<!-- 图片2 -->
<ImageView
android:id="@+id/iv_img2"
style="@style/ivImg"
android:layout_margin="2dp" />
<!-- 图片3 -->
<ImageView
android:id="@+id/iv_img3"
style="@style/ivImg"
android:layout_margin="2dp" />
</LinearLayout>
<!-- 底部信息布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal">
<!-- 用户名 -->
<TextView
android:id="@+id/tv_name"
style="@style/tvInfo"
android:text="央视新闻客户端" />
<!-- 评论数 -->
<TextView
android:id="@+id/tv_comment"
style="@style/tvInfo"
android:text="9884评" />
<!-- 发布时间 -->
<TextView
android:id="@+id/tv_time"
style="@style/tvInfo"
android:text="6小时前" />
</LinearLayout>
</LinearLayout>
4.4.3 核心控件与属性解析
-
根布局:LinearLayout(垂直)
orientation="vertical":标题在上,图片在中,底部信息在下- 其他属性与单图布局一致,保证条目样式统一
-
新闻标题 TextView
- 与单图布局的标题样式完全一致,保证 UI 统一
maxLines="2"、ellipsize="end"限制标题显示
-
三张图片布局
- 横向线性布局,3 个 ImageView 全部引用
@style/ivImg layout_weight="1"让 3 张图片等宽平分屏幕layout_margin="2dp":图片之间保持间距,避免粘连
- 横向线性布局,3 个 ImageView 全部引用
-
底部信息布局
- 与单图布局的底部信息完全一致,引用
@style/tvInfo,保证样式统一 - 用户名、评论数、时间横向排列,层级清晰
- 与单图布局的底部信息完全一致,引用
五、数据实体类:NewsBean.java
5.1 实体类作用
NewsBean 是数据模型类,用于封装每条新闻的所有属性,作为 RecyclerView 适配器的数据载体。它遵循 JavaBean 规范,包含私有字段、getter/setter 方法。
5.2 完整代码解析
java
运行
package cn.itcast.headline;
import java.util.List;
public class NewsBean {
private int id; // 新闻id
private String title; // 新闻标题
private List<Integer> imgList; // 新闻图片(存储图片资源id)
private String name; // 用户名
private String comment; // 用户评论数
private String time; // 新闻发布时间
private int type; // 新闻类型:1=单图/无图,2=三图
// 新闻id的getter/setter
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
// 新闻标题的getter/setter
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
// 用户名的getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 评论数的getter/setter
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
// 发布时间的getter/setter
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
// 图片列表的getter/setter
public List<Integer> getImgList() {
return imgList;
}
public void setImgList(List<Integer> imgList) {
this.imgList = imgList;
}
// 新闻类型的getter/setter
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
5.3 核心字段解析
表格
| 字段名 | 类型 | 作用 |
|---|---|---|
id | int | 新闻唯一标识,用于区分不同条目 |
title | String | 新闻标题,显示在条目顶部 |
imgList | List | 存储图片资源 ID 的列表,单图条目存 1 个,三图条目存 3 个,无图条目存空 |
name | String | 用户名,显示在条目底部 |
comment | String | 评论数,如 “9884 评” |
time | String | 发布时间,如 “6 小时前” |
type | int | 条目类型:1 对应单图 / 无图布局,2 对应三图布局,用于适配器区分条目样式 |
5.4 设计思路
- 用
List<Integer>存储图片,灵活适配单图、三图、无图三种场景 - 用
type字段标记条目类型,让适配器可以根据类型加载不同布局 - 遵循 JavaBean 规范,所有字段私有,通过 getter/setter 访问,保证数据安全
六、核心适配器:NewsAdapter.java 深度解析
适配器是 RecyclerView 的 “灵魂”,本项目的适配器需要处理多类型条目、ViewHolder 复用、数据绑定,是整个项目最核心的部分。
6.1 适配器核心结构
NewsAdapter 继承自 RecyclerView.Adapter<RecyclerView.ViewHolder>,因为有两种 ViewHolder,所以泛型用父类 RecyclerView.ViewHolder。
适配器包含 4 个核心方法:
onCreateViewHolder:创建 ViewHolder,根据类型加载不同布局getItemViewType:返回条目类型,用于onCreateViewHolder区分布局onBindViewHolder:绑定数据到 ViewHoldergetItemCount:返回条目总数- 两个内部 ViewHolder 类:
MyViewViewHolder1、MyViewViewHolder2
6.2 完整代码逐行解析
java
运行
package cn.itcast.headline;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
// 适配器继承RecyclerView.Adapter,泛型用父类ViewHolder,支持多类型
public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mContext;
private List<NewsBean> NewsList;
// 构造方法:传入上下文和数据列表
public NewsAdapter(Context context, List<NewsBean> NewsList) {
this.mContext = context;
this.NewsList = NewsList;
}
// 1. 根据条目类型创建ViewHolder
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = null;
RecyclerView.ViewHolder holder = null;
// 类型1:加载单图/无图布局
if (viewType == 1) {
itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_one, parent, false);
holder = new MyViewViewHolder1(itemView);
}
// 类型2:加载三图布局
else if (viewType == 2) {
itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_two, parent, false);
holder = new MyViewViewHolder2(itemView);
}
return holder;
}
// 2. 返回条目类型,从NewsBean的type字段获取
@Override
public int getItemViewType(int position) {
return NewsList.get(position).getType();
}
// 3. 绑定数据到ViewHolder
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
NewsBean bean = NewsList.get(position);
// 判断ViewHolder类型,绑定对应数据
if (holder instanceof MyViewViewHolder1) {
// 单图/无图ViewHolder
MyViewViewHolder1 viewHolder1 = (MyViewViewHolder1) holder;
// 绑定标题、用户名、评论数、时间
viewHolder1.title.setText(bean.getTitle());
viewHolder1.name.setText(bean.getName());
viewHolder1.comment.setText(bean.getComment());
viewHolder1.time.setText(bean.getTime());
// 控制置顶图标和图片的显示/隐藏
if (position == 0) {
// 第一条目:置顶,隐藏新闻图片,显示置顶图标
viewHolder1.iv_top.setVisibility(View.VISIBLE);
viewHolder1.iv_img.setVisibility(View.GONE);
} else {
// 其他条目:显示新闻图片,隐藏置顶图标
viewHolder1.iv_top.setVisibility(View.GONE);
// 图片列表不为空则显示图片
if (bean.getImgList() != null && bean.getImgList().size() > 0) {
viewHolder1.iv_img.setVisibility(View.VISIBLE);
viewHolder1.iv_img.setImageResource(bean.getImgList().get(0));
} else {
viewHolder1.iv_img.setVisibility(View.GONE);
}
}
} else if (holder instanceof MyViewViewHolder2) {
// 三图ViewHolder
MyViewViewHolder2 viewHolder2 = (MyViewViewHolder2) holder;
// 绑定标题、用户名、评论数、时间
viewHolder2.title.setText(bean.getTitle());
viewHolder2.name.setText(bean.getName());
viewHolder2.comment.setText(bean.getComment());
viewHolder2.time.setText(bean.getTime());
// 绑定3张图片
if (bean.getImgList() != null && bean.getImgList().size() >= 3) {
viewHolder2.iv_img1.setImageResource(bean.getImgList().get(0));
viewHolder2.iv_img2.setImageResource(bean.getImgList().get(1));
viewHolder2.iv_img3.setImageResource(bean.getImgList().get(2));
}
}
}
// 4. 返回条目总数
@Override
public int getItemCount() {
return NewsList.size();
}
// 单图/无图ViewHolder:缓存条目内的控件
class MyViewViewHolder1 extends RecyclerView.ViewHolder {
ImageView iv_top, iv_img;
TextView title, name, comment, time;
public MyViewViewHolder1(View itemView) {
super(itemView);
// 绑定控件ID,只执行一次,避免重复findViewById
iv_top = itemView.findViewById(R.id.iv_top);
iv_img = itemView.findViewById(R.id.iv_img);
title = itemView.findViewById(R.id.tv_title);
name = itemView.findViewById(R.id.tv_name);
comment = itemView.findViewById(R.id.tv_comment);
time = itemView.findViewById(R.id.tv_time);
}
}
// 三图ViewHolder:缓存条目内的控件
class MyViewViewHolder2 extends RecyclerView.ViewHolder {
ImageView iv_img1, iv_img2, iv_img3;
TextView title, name, comment, time;
public MyViewViewHolder2(View itemView) {
super(itemView);
// 绑定控件ID
iv_img1 = itemView.findViewById(R.id.iv_img1);
iv_img2 = itemView.findViewById(R.id.iv_img2);
iv_img3 = itemView.findViewById(R.id.iv_img3);
title = itemView.findViewById(R.id.tv_title);
name = itemView.findViewById(R.id.tv_name);
comment = itemView.findViewById(R.id.tv_comment);
time = itemView.findViewById(R.id.tv_time);
}
}
}
6.3 核心方法深度解析
6.3.1 构造方法
java
运行
public NewsAdapter(Context context, List<NewsBean> NewsList) {
this.mContext = context;
this.NewsList = NewsList;
}
- 传入
Context:用于加载布局(LayoutInflater) - 传入
List<NewsBean>:数据列表,适配器的数据源
6.3.2 getItemViewType 方法
java
运行
@Override
public int getItemViewType(int position) {
return NewsList.get(position).getType();
}
- 作用:返回当前条目的类型,
onCreateViewHolder根据这个类型加载不同布局 - 本项目中:
1对应单图 / 无图,2对应三图 - 注意:该方法在
onCreateViewHolder之前调用,必须正确返回类型,否则会加载错误布局
6.3.3 onCreateViewHolder 方法
java
运行
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = null;
RecyclerView.ViewHolder holder = null;
if (viewType == 1) {
itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_one, parent, false);
holder = new MyViewViewHolder1(itemView);
} else if (viewType == 2) {
itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_two, parent, false);
holder = new MyViewViewHolder2(itemView);
}
return holder;
}
-
作用:创建条目视图,封装为 ViewHolder,只在需要新 ViewHolder 时调用(条目首次出现、复用池为空时)
-
核心细节:
LayoutInflater.inflate第三个参数必须为false:因为 RecyclerView 会自动将条目添加到父布局,设为true会导致重复添加,报错- 根据
viewType加载不同布局,创建对应 ViewHolder - ViewHolder 只创建一次,后续滚动时复用,避免重复 inflate 布局,提升性能
6.3.4 onBindViewHolder 方法
java
运行
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
NewsBean bean = NewsList.get(position);
if (holder instanceof MyViewViewHolder1) {
MyViewViewHolder1 viewHolder1 = (MyViewViewHolder1) holder;
viewHolder1.title.setText(bean.getTitle());
viewHolder1.name.setText(bean.getName());
viewHolder1.comment.setText(bean.getComment());
viewHolder1.time.setText(bean.getTime());
// 控制置顶和图片显示
if (position == 0) {
viewHolder1.iv_top.setVisibility(View.VISIBLE);
viewHolder1.iv_img.setVisibility(View.GONE);
} else {
viewHolder1.iv_top.setVisibility(View.GONE);
if (bean.getImgList() != null && bean.getImgList().size() > 0) {
viewHolder1.iv_img.setVisibility(View.VISIBLE);
viewHolder1.iv_img.setImageResource(bean.getImgList().get(0));
} else {
viewHolder1.iv_img.setVisibility(View.GONE);
}
}
} else if (holder instanceof MyViewViewHolder2) {
MyViewViewHolder2 viewHolder2 = (MyViewViewHolder2) holder;
viewHolder2.title.setText(bean.getTitle());
viewHolder2.name.setText(bean.getName());
viewHolder2.comment.setText(bean.getComment());
viewHolder2.time.setText(bean.getTime());
if (bean.getImgList() != null && bean.getImgList().size() >= 3) {
viewHolder2.iv_img1.setImageResource(bean.getImgList().get(0));
viewHolder2.iv_img2.setImageResource(bean.getImgList().get(1));
viewHolder2.iv_img3.setImageResource(bean.getImgList().get(2));
}
}
}
-
作用:将数据绑定到 ViewHolder 的控件上,每次条目显示时都会调用(包括复用)
-
核心细节:
- 通过
instanceof判断 ViewHolder 类型,强转后绑定数据 - 单图 ViewHolder 中,通过
position == 0判断是否为置顶条目,控制iv_top和iv_img的显示 / 隐藏 - 图片绑定前做非空判断,避免空指针异常
- 三图 ViewHolder 中,绑定 3 张图片,从
imgList中按索引获取
- 通过
6.3.5 getItemCount 方法
java
运行
@Override
public int getItemCount() {
return NewsList.size();
}
- 作用:返回条目总数,RecyclerView 根据这个值决定绘制多少个条目
- 必须返回正确的数量,否则会出现空白条目或崩溃
6.3.6 ViewHolder 内部类
java
运行
class MyViewViewHolder1 extends RecyclerView.ViewHolder {
ImageView iv_top, iv_img;
TextView title, name, comment, time;
public MyViewViewHolder1(View itemView) {
super(itemView);
iv_top = itemView.findViewById(R.id.iv_top);
iv_img = itemView.findViewById(R.id.iv_img);
title = itemView.findViewById(R.id.tv_title);
name = itemView.findViewById(R.id.tv_name);
comment = itemView.findViewById(R.id.tv_comment);
time = itemView.findViewById(R.id.tv_time);
}
}
- 作用:缓存条目内的所有控件,
findViewById只在 ViewHolder 创建时执行一次,后续复用直接使用缓存的控件,大幅提升性能 - 两个 ViewHolder 分别对应两种条目布局,缓存各自的控件
6.4 多类型条目实现原理
本项目的多类型条目实现,核心是 3 个步骤:
- 数据层:
NewsBean中添加type字段,标记每个条目的类型 - 适配器层:重写
getItemViewType,返回type;onCreateViewHolder根据type加载不同布局,创建不同 ViewHolder - 绑定层:
onBindViewHolder根据 ViewHolder 类型,绑定对应数据
这种实现方式是 RecyclerView 多类型条目的标准方案,逻辑清晰、可维护性高。
七、主 Activity:MainActivity.java 数据初始化与列表绑定
7.1 Activity 作用
MainActivity 是项目的入口,负责:
- 初始化布局,找到 RecyclerView 控件
- 初始化新闻数据,封装为
NewsBean列表 - 创建适配器,设置布局管理器,绑定 RecyclerView
- 显示列表数据
7.2 完整代码逐行解析
java
运行
package cn.itcast.headline;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
// 新闻标题数组
private String[] titles = {
"各地餐企齐行动,杜绝餐饮浪费",
"花菜有人焯水,都错了,有人直接炒,都错了,看饭店大厨怎么做",
"睡觉时,双脚突然蹬一下,有踩空感,像从高楼坠落,是咋回事?",
"实拍外卖小哥砸开小吃店的卷帘门救火,灭火后淡定继续外卖",
"风调雨顺果实大丰收:果农喜上眉梢,8元一斤抢购难,供应无法满足需求",
"大会,大展,大赛一起来,北京电竞“好嗨哟”"
};
// 用户名数组
private String[] names = {
"央视新闻客户端",
"味美食记",
"民康健康",
"生活小记",
"禾木报告",
"燕鸣"
};
// 评论数数组
private String[] comments = {
"9884评",
"18评",
"78评",
"678评",
"189评",
"304评"
};
// 发布时间数组
private String[] times = {
"6小时前",
"刚刚",
"1小时前",
"2小时前",
"3小时前",
"4小时前"
};
// 单图条目图片数组(1张图片)
private int[] icons1 = {
R.drawable.food,
R.drawable.takeout
};
// 三图条目图片数组(3张图片)
private int[] icons2 = {
R.drawable.sleep1,
R.drawable.sleep2,
R.drawable.sleep3,
R.drawable.e_sports,
R.drawable.fruit1,
R.drawable.fruit2,
R.drawable.fruit3
};
// 新闻类型数组:1=单图/无图,2=三图
private int[] types = {1, 1, 2, 1, 2, 1};
// RecyclerView控件
private RecyclerView mRecyclerView;
// 适配器
private NewsAdapter mAdapter;
// 数据列表
private List<NewsBean> NewsList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化数据
setData();
// 找到RecyclerView控件
mRecyclerView = findViewById(R.id.rv_list);
// 设置布局管理器:线性布局,垂直
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 创建适配器,传入上下文和数据列表
mAdapter = new NewsAdapter(MainActivity.this, NewsList);
// 给RecyclerView设置适配器
mRecyclerView.setAdapter(mAdapter);
}
// 初始化数据方法
private void setData() {
NewsList = new ArrayList<>();
NewsBean bean;
// 遍历标题数组,创建NewsBean,添加到列表
for (int i = 0; i < titles.length; i++) {
bean = new NewsBean();
bean.setId(i + 1);
bean.setTitle(titles[i]);
bean.setName(names[i]);
bean.setComment(comments[i]);
bean.setTime(times[i]);
bean.setType(types[i]);
// 根据索引和类型,设置图片列表
switch (i) {
case 0:
// 第1条:置顶,无图片
List<Integer> imgList0 = new ArrayList<>();
bean.setImgList(imgList0);
break;
case 1:
// 第2条:单图
List<Integer> imgList1 = new ArrayList<>();
imgList1.add(icons1[i - 1]);
bean.setImgList(imgList1);
break;
case 2:
// 第3条:三图
List<Integer> imgList2 = new ArrayList<>();
imgList2.add(icons2[i - 2]);
imgList2.add(icons2[i - 1]);
imgList2.add(icons2[i]);
bean.setImgList(imgList2);
break;
case 3:
// 第4条:单图
List<Integer> imgList3 = new ArrayList<>();
imgList3.add(icons1[i - 2]);
bean.setImgList(imgList3);
break;
case 4:
// 第5条:三图
List<Integer> imgList4 = new ArrayList<>();
imgList4.add(icons2[i - 1]);
imgList4.add(icons2[i]);
imgList4.add(icons2[i + 1]);
bean.setImgList(imgList4);
break;
case 5:
// 第6条:单图
List<Integer> imgList5 = new ArrayList<>();
imgList5.add(icons1[i - 3]);
bean.setImgList(imgList5);
break;
}
// 将bean添加到数据列表
NewsList.add(bean);
}
}
}
7.3 核心代码解析
7.3.1 数据数组定义
java
运行
private String[] titles = {...};
private String[] names = {...};
private String[] comments = {...};
private String[] times = {...};
private int[] icons1 = {...};
private int[] icons2 = {...};
private int[] types = {1, 1, 2, 1, 2, 1};
- 定义 6 个数组,分别存储标题、用户名、评论数、时间、单图图片、三图图片、条目类型
types数组标记每个条目的类型,对应NewsBean的type字段
7.3.2 onCreate 方法
java
运行
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setData();
mRecyclerView = findViewById(R.id.rv_list);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new NewsAdapter(MainActivity.this, NewsList);
mRecyclerView.setAdapter(mAdapter);
}
-
核心步骤:
setContentView:加载主布局setData():初始化数据,创建NewsListfindViewById:找到 RecyclerView 控件setLayoutManager:设置线性布局管理器,垂直列表- 创建适配器,传入上下文和数据列表
setAdapter:给 RecyclerView 设置适配器,显示列表
7.3.3 setData 方法
java
运行
private void setData() {
NewsList = new ArrayList<>();
NewsBean bean;
for (int i = 0; i < titles.length; i++) {
bean = new NewsBean();
bean.setId(i + 1);
bean.setTitle(titles[i]);
bean.setName(names[i]);
bean.setComment(comments[i]);
bean.setTime(times[i]);
bean.setType(types[i]);
switch (i) {
case 0:
List<Integer> imgList0 = new ArrayList<>();
bean.setImgList(imgList0);
break;
case 1:
List<Integer> imgList1 = new ArrayList<>();
imgList1.add(icons1[i - 1]);
bean.setImgList(imgList1);
break;
case 2:
List<Integer> imgList2 = new ArrayList<>();
imgList2.add(icons2[i - 2]);
imgList2.add(icons2[i - 1]);
imgList2.add(icons2[i]);
bean.setImgList(imgList2);
break;
// 其他case...
}
NewsList.add(bean);
}
}
-
作用:遍历数组,创建
NewsBean对象,设置属性,添加到NewsList -
核心细节:
-
通过
switch区分不同条目的图片设置:- 第 1 条:空列表,无图(置顶)
- 第 2、4、6 条:单图,从
icons1取图片 - 第 3、5 条:三图,从
icons2取 3 张图片
-
每个
NewsBean设置type,对应types数组 -
最后将
bean添加到NewsList,作为适配器的数据源
-
八、ListView 与 RecyclerView 对比:为什么 RecyclerView 是更好的选择
很多开发者在学习时会疑惑:既然 ListView 也能实现列表,为什么要学 RecyclerView?这里我们做一个全面对比,帮你彻底理解两者的差异。
8.1 性能对比
表格
| 维度 | ListView | RecyclerView |
|---|---|---|
| 视图复用 | 手动实现 convertView 和 ViewHolder,易出错,新手常忘记复用 | 内置 ViewHolder 模式,强制复用,性能更稳定 |
| 布局效率 | 每次滚动都需要重新测量布局,效率低 | 布局管理器统一测量,支持预取,滚动更流畅 |
| 内存占用 | 无视图回收机制,快速滚动时内存占用高 | 自动回收不可见视图,内存占用更低 |
| 局部刷新 | 无原生支持,需手动刷新整个列表 | 支持 notifyItemChanged 等局部刷新,性能更优 |
8.2 功能对比
表格
| 功能 | ListView | RecyclerView |
|---|---|---|
| 布局方向 | 仅支持垂直列表,横向需自定义 | 支持垂直、水平、网格、瀑布流,灵活度高 |
| 条目动画 | 无原生支持,需自定义 | 内置 ItemAnimator,支持增删改查动画 |
| 多类型条目 | 需手动在 getView 中判断,逻辑复杂 | 原生支持 getItemViewType,实现优雅 |
| 分割线 | 需手动设置 divider,样式单一 | 可自定义 ItemDecoration,灵活实现分割线、吸顶等效果 |
| 拖拽与滑动删除 | 无原生支持 | 支持 ItemTouchHelper,快速实现拖拽、侧滑删除 |
8.3 本项目中 RecyclerView 的优势
在本项目中,RecyclerView 的优势体现得淋漓尽致:
- 多类型条目:原生支持两种条目布局,代码逻辑清晰,ListView 实现会非常繁琐
- 视图复用:强制 ViewHolder 复用,避免 ListView 中新手常犯的 “忘记复用” 导致的性能问题
- 布局灵活:线性布局管理器轻松实现垂直列表,后续可快速改为网格、瀑布流
- 性能优化:滚动流畅,即使条目多也不会卡顿,ListView 快速滚动易出现卡顿
九、RecyclerView 常见问题与优化方案
9.1 常见问题与解决方案
9.1.1 条目点击事件
RecyclerView 没有原生的 onItemClickListener,需要在适配器中自定义:
java
运行
// 1. 定义点击回调接口
public interface OnItemClickListener {
void onItemClick(int position, NewsBean bean);
}
// 2. 在适配器中添加回调变量
private OnItemClickListener listener;
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
// 3. 在ViewHolder中设置点击事件
class MyViewViewHolder1 extends RecyclerView.ViewHolder {
public MyViewViewHolder1(View itemView) {
super(itemView);
itemView.setOnClickListener(v -> {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION) {
listener.onItemClick(position, NewsList.get(position));
}
});
}
}
// 4. 在Activity中设置监听
mAdapter.setOnItemClickListener((position, bean) -> {
// 处理点击事件
Toast.makeText(this, bean.getTitle(), Toast.LENGTH_SHORT).show();
});
9.1.2 条目闪烁问题
当调用 notifyDataSetChanged 时,整个列表会刷新,出现闪烁。解决方案:
- 使用差分更新
DiffUtil,只刷新变化的条目:
java
运行
public class NewsDiffCallback extends DiffUtil.Callback {
private List<NewsBean> oldList;
private List<NewsBean> newList;
public NewsDiffCallback(List<NewsBean> oldList, List<NewsBean> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
// 判断是否为同一个条目(用id)
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
// 判断内容是否相同
NewsBean oldBean = oldList.get(oldItemPosition);
NewsBean newBean = newList.get(newItemPosition);
return oldBean.getTitle().equals(newBean.getTitle())
&& oldBean.getComment().equals(newBean.getComment());
}
}
// 更新数据时使用
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new NewsDiffCallback(oldList, newList));
diffResult.dispatchUpdatesTo(mAdapter);
9.1.3 图片加载卡顿
本项目中图片直接用 setImageResource,在列表中快速滚动会出现卡顿。解决方案:
- 使用图片加载框架(如 Glide、Picasso),异步加载图片:
java
运行
// 以Glide为例
Glide.with(mContext)
.load(bean.getImgList().get(0))
.into(viewHolder1.iv_img);
9.2 性能优化方案
- 使用 ViewHolder 模式:本项目已经实现,避免重复
findViewById - 避免在 onBindViewHolder 中做耗时操作:如网络请求、复杂计算,异步处理
- 使用 DiffUtil 局部刷新:代替
notifyDataSetChanged,减少刷新范围 - 设置固定大小:
mRecyclerView.setHasFixedSize(true),避免重复测量布局 - 使用图片加载框架:异步加载,缓存图片,避免卡顿
- 优化布局层级:使用
ConstraintLayout减少布局嵌套,提升测量效率 - 预取优化:
RecyclerView.LayoutManager.setInitialPrefetchItemCount,预取条目,提升滚动流畅度
十、项目总结与拓展
10.1 项目核心知识点总结
通过本项目,我们系统学习了 RecyclerView 的完整使用流程,核心知识点包括:
- RecyclerView 核心原理与四大组件:控件、LayoutManager、Adapter、ViewHolder
- 多类型条目实现:通过
getItemViewType区分布局,适配不同样式 - 布局设计:主布局、标题栏、单图 / 三图条目布局的设计与控件使用
- 数据封装:JavaBean 实体类封装新闻数据
- 适配器开发:多类型适配器的完整实现,ViewHolder 复用
- 样式与主题配置:统一 UI 风格,抽取样式与颜色
- ListView 与 RecyclerView 对比:理解技术选型的原因
10.2 项目拓展方向
本项目是一个基础的 RecyclerView 实战,你可以基于此进行拓展,实现更复杂的功能:
- 添加网络请求:从服务器获取新闻数据,代替本地数组
- 实现下拉刷新、上拉加载更多:使用 SwipeRefreshLayout + RecyclerView
- 添加条目动画:使用
DefaultItemAnimator或自定义动画 - 实现侧滑删除、拖拽排序:使用
ItemTouchHelper - 添加分割线:自定义
ItemDecoration,实现美观的分割线 - 优化图片加载:集成 Glide,实现图片缓存、圆角、占位图
- 实现吸顶标题:自定义
ItemDecoration,实现分类标题吸顶效果 - 适配深色模式:根据系统主题切换颜色样式
结语
RecyclerView 是 Android 开发中最核心的控件之一,掌握它的原理和使用,是每个 Android 开发者的必备技能。
本文通过仿今日头条列表项目,从基础原理到代码实现,从布局设计到适配器开发,全方位拆解了 RecyclerView 的使用流程,希望能帮你彻底吃透这个控件。
部分图片展示