RecyclerView 列表实现
深入 RecyclerView 全家桶:Adapter/ViewHolder/LayoutManager/ItemDecoration/DiffUtil/ItemTouchHelper 与四级缓存。
学习目标
- 理解 Adapter + ViewHolder 模式与 RecyclerView 的职责分离
- 能用 LinearLayoutManager / GridLayoutManager 实现线性与网格列表
- 会用 ItemDecoration 画分割线和间距
- 掌握 DiffUtil 高效局部刷新替代 notifyDataSetChanged
- 能用 ItemTouchHelper 实现拖拽排序与侧滑删除
- 理解 RecyclerView 四级缓存的工作机制
学习目标
- 能解释 Adapter、ViewHolder、LayoutManager 三者各自负责什么
- 能用
LinearLayoutManager/GridLayoutManager搭出可运行的线性/网格列表 - 能用
ItemDecoration画分割线和间距,区别addItemDecoration 与 margin - 会用
DiffUtil+ListAdapter做局部刷新,告别notifyDataSetChanged - 能用
ItemTouchHelper实现长按拖拽和侧滑删除 - 理解 RecyclerView 的四级缓存(Scrap / Cache / ViewCacheExtension / RecycledViewPool)
RecyclerView 列表实现
1. 为什么是 RecyclerView
老的 ListView 有几个硬伤:只能竖向滚动、getView 里 findViewById 频繁 IO、没有 Diff 局部刷新、无法横向网格。RecyclerView 把职责拆开成多个独立组件,整体变得灵活、可复用、可插拔:
| 组件 | 职责 |
|---|---|
RecyclerView |
容器,管理布局、动画、滚动 |
Adapter |
把数据映射成 ViewHolder |
ViewHolder |
持有 Item 的 View 引用,避免反复 findViewById |
LayoutManager |
决定 Item 怎么摆(线性、网格、瀑布流) |
ItemDecoration |
给 Item 画分割线、间距 |
ItemAnimator |
增删改时的动画 |
ItemTouchHelper |
拖拽与侧滑手势 |
依赖:
implementation 'androidx.recyclerview:recyclerview:1.3.2'
2. Adapter + ViewHolder 基础示例
先定义数据模型和 Item 布局。
item_text.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:padding="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp"
android:textColor="@color/text_secondary" />
</LinearLayout>
数据模型:
package com.example.recyclerviewdemo;
public class Note {
private final String title;
private final String subtitle;
public Note(String title, String subtitle) {
this.title = title;
this.subtitle = subtitle;
}
public String getTitle() { return title; }
public String getSubtitle() { return subtitle; }
}
Adapter:
package com.example.recyclerviewdemo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class NoteAdapter extends RecyclerView.Adapter<NoteAdapter.NoteVH> {
private final List<Note> data = new ArrayList<>();
@NonNull
@Override
public NoteVH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_text, parent, false);
return new NoteVH(view);
}
@Override
public void onBindViewHolder(@NonNull NoteVH holder, int position) {
Note note = data.get(position);
holder.tvTitle.setText(note.getTitle());
holder.tvSubtitle.setText(note.getSubtitle());
}
@Override
public int getItemCount() {
return data.size();
}
public void setItems(List<Note> items) {
data.clear();
data.addAll(items);
notifyDataSetChanged();
}
static class NoteVH extends RecyclerView.ViewHolder {
TextView tvTitle;
TextView tvSubtitle;
NoteVH(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tvTitle);
tvSubtitle = itemView.findViewById(R.id.tvSubtitle);
}
}
}
注意几个细节:
inflate(R.layout.item_text, parent, false)的第三个参数必须是false——传true会把 root 加进 parent 两次,导致测量尺寸异常ViewHolder内部只做一次findViewById,后续onBindViewHolder只调setText等便宜的方法ViewHolder强烈建议写成 Adapter 的static内部类,避免隐式持有外部 Adapter 引用导致内存泄漏
2.1 RecyclerView Activity 接线
package com.example.recyclerviewdemo;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView rv = findViewById(R.id.recyclerView);
rv.setLayoutManager(new LinearLayoutManager(this));
NoteAdapter adapter = new NoteAdapter();
rv.setAdapter(adapter);
List<Note> notes = new ArrayList<>();
for (int i = 0; i < 30; i++) {
notes.add(new Note("标题 " + (i + 1), "这是第 " + (i + 1) + 条副标题"));
}
adapter.setItems(notes);
}
}
布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
clipToPadding="false" 让内容能滚到 padding 区域,避免最后一项被 padding 截断。
3. LayoutManager:线性、网格、瀑布流
3.1 LinearLayoutManager
// 垂直
rv.setLayoutManager(new LinearLayoutManager(this));
// 横向
rv.setLayoutManager(
new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
// 反向(数据从底部开始)
rv.setLayoutManager(
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true));
3.2 GridLayoutManager
rv.setLayoutManager(new GridLayoutManager(this, 3));
// 列宽自适应:用 ViewCompat 设置 spanSize
需要某些 Item 跨多列(如 Banner 占满整行)时用 SpanSizeLookup:
GridLayoutManager lm = new GridLayoutManager(this, 3);
lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
// 头部 Banner 占 3 列,其他占 1 列
return position == 0 ? 3 : 1;
}
});
rv.setLayoutManager(lm);
3.3 StaggeredGridLayoutManager 瀑布流
rv.setLayoutManager(
new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
瀑布流的子 Item 高度允许不一致,由系统测量后排列。
4. ItemDecoration:分割线与间距
ItemDecoration 让你在每个 Item 的四周画装饰。最常见的是分割线。
4.1 简单间距
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private final int space;
public SpaceItemDecoration(int space) { this.space = space; }
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
// 第一项上方也加,避免和顶部 padding 叠成两倍
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = space;
}
}
}
outRect 是一个 Rect,给 Item 四周增加偏移量(相当于额外的 margin,但不会撑大自己)。使用:
rv.addItemDecoration(new SpaceItemDecoration(8));
4.2 自带 DividerItemDecoration
DividerItemDecoration decoration = new DividerItemDecoration(this, LinearLayoutManager.VERTICAL);
decoration.setDrawable(getDrawable(R.drawable.divider_default));
rv.addItemDecoration(decoration);
divider_default.xml:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="#FFEEEEEE" />
</shape>
注意 DividerItemDecoration 默认只画水平列表的竖直分割线或竖直列表的水平分割线,对 GridLayoutManager 不适用——网格得自己写 ItemDecoration 在 onDraw 里画线。
5. ItemAnimator 默认动画
RecyclerView 自带 DefaultItemAnimator,增删 Item 时有 fade / translation 动画。只在你调用 notifyItemInserted / notifyItemRemoved / notifyItemRangeChanged 时才生效,notifyDataSetChanged 会跳过动画。
关闭动画:
rv.setItemAnimator(null);
调整动画时长:
DefaultItemAnimator animator = new DefaultItemAnimator();
animator.setAddDuration(200);
animator.setRemoveDuration(200);
rv.setItemAnimator(animator);
6. 多类型 Item
需求:列表头部是 Banner(布局 A),中间是普通条目(布局 B)。核心是 getItemViewType 返回不同 type,onCreateViewHolder 根据 type inflate 不同布局。
public class MixedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_BANNER = 1;
private static final int TYPE_NOTE = 2;
private final List<Object> items = new ArrayList<>(); // 实际项目用 sealed class 或类型安全封装
@Override
public int getItemViewType(int position) {
Object item = items.get(position);
if (item instanceof Banner) return TYPE_BANNER;
return TYPE_NOTE;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == TYPE_BANNER) {
return new BannerVH(inflater.inflate(R.layout.item_banner, parent, false));
}
return new NoteVH(inflater.inflate(R.layout.item_text, parent, false));
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Object item = items.get(position);
if (holder instanceof BannerVH) {
((BannerVH) holder).bind((Banner) item);
} else if (holder instanceof NoteVH) {
((NoteVH) holder).bind((Note) item);
}
}
@Override
public int getItemCount() { return items.size(); }
static class BannerVH extends RecyclerView.ViewHolder {
ImageView ivBanner;
BannerVH(@NonNull View itemView) {
super(itemView);
ivBanner = itemView.findViewById(R.id.ivBanner);
}
void bind(Banner b) { /* ... */ }
}
static class NoteVH extends RecyclerView.ViewHolder {
TextView tvTitle, tvSubtitle;
NoteVH(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tvTitle);
tvSubtitle = itemView.findViewById(R.id.tvSubtitle);
}
void bind(Note n) {
tvTitle.setText(n.getTitle());
tvSubtitle.setText(n.getSubtitle());
}
}
}
getItemViewType 返回的 type 值会作为 onCreateViewHolder 的 viewType 参数,也参与 RecycledViewPool 的缓存区分(不同 type 不复用同一 ViewHolder)。
7. DiffUtil 高效更新
每次 notifyDataSetChanged 会触发所有可见 Item 重新 bind,且丢失动画。DiffUtil 用 LCS(最长公共子序列)算法,只对真正变化的 Item 发精准 notify,保留动画。
7.1 ListAdapter 模板
AndroidX 提供了 ListAdapter 基类,把 DiffUtil 调用全部封装好:
public class NoteAdapter extends ListAdapter<Note, RecyclerView.ViewHolder> {
protected NoteAdapter() {
super(DIFF);
}
private static final DiffUtil.ItemCallback<Note> DIFF =
new DiffUtil.ItemCallback<Note>() {
@Override
public boolean areItemsTheSame(@NonNull Note oldItem, @NonNull Note newItem) {
// 用来判定「是不是同一条」——通常用 id
return oldItem.getTitle().equals(newItem.getTitle());
}
@Override
public boolean areContentsTheSame(@NonNull Note oldItem, @NonNull Note newItem) {
// 同一条但内容可能变了——通常用 equals 或字段比较
return oldItem.getTitle().equals(newItem.getTitle())
&& oldItem.getSubtitle().equals(newItem.getSubtitle());
}
};
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 同上
return null;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
// 注意:用 getItem(position) 取数据,不再是 data.get
Note note = getItem(position);
// ...
}
}
提交新数据时一行:
adapter.submitList(newNotes);
submitList 内部异步跑 DiffUtil,比 notifyDataSetChanged 快得多,且保留 ItemAnimator 动画。
7.2 高级:payload 增量更新
如果只想刷新某个字段而非整行 bind,实现 getChangePayload:
@Override
public Object getChangePayload(@NonNull Note oldItem, @NonNull Note newItem) {
Bundle diff = new Bundle();
if (!oldItem.getSubtitle().equals(newItem.getSubtitle())) {
diff.putString("subtitle", newItem.getSubtitle());
}
return diff.isEmpty() ? super.getChangePayload(oldItem, newItem) : diff;
}
然后在 onBindViewHolder 的三参重载里处理 payload:
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position,
@NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
return;
}
Bundle diff = (Bundle) payloads.get(0);
if (diff.containsKey("subtitle")) {
// 只刷新副标题,不重新 bind 整行
((NoteVH) holder).tvSubtitle.setText(diff.getString("subtitle"));
}
}
这是 RecyclerView 性能优化的精髓之一,但使用前请用 Layout Inspector 确认你确实有性能瓶颈——大多数场景 submitList 已经够快。
8. ItemTouchHelper 拖拽与侧滑
ItemTouchHelper 内置了拖拽排序和侧滑删除,给 Adapter 加 ItemTouchHelper.Callback 即可。
ItemTouchHelper.SimpleCallback cb = new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN, // 拖拽方向
ItemTouchHelper.START | ItemTouchHelper.END // 侧滑方向
) {
@Override
public boolean onMove(@NonNull RecyclerView rv, @NonNull RecyclerView.ViewHolder vh,
@NonNull RecyclerView.ViewHolder target) {
int from = vh.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(data, from, to);
adapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder vh, int direction) {
int pos = vh.getAdapterPosition();
data.remove(pos);
adapter.notifyItemRemoved(pos);
}
@Override
public boolean isLongPressDragEnabled() {
return true; // 长按启用拖拽
}
};
ItemTouchHelper helper = new ItemTouchHelper(cb);
helper.attachToRecyclerView(rv);
注意:
onMove里要同时改数据并notifyItemMoved,二者必须同步,否则下次 DiffUtil 会算错onSwiped侧滑删除也要同步删数据,否则 Item 会「弹回」但数据已变- 关掉长按拖拽(
isLongPressDragEnabled=false)后,需要给某个 handle 绑定helper.startDrag(vh)才能拖动
9. 缓存机制:四级缓存
理解缓存能让你的列表又快又省内存。RecyclerView 内部维护 4 级缓存:
| 层级 | 名字 | 容量 | 作用 |
|---|---|---|---|
| 1 | Scrap(Attached Scrap) | 无限 | 当前还附着在屏幕上、临时 detach 的 ViewHolder,屏幕回滚立即复用,不重新 bind |
| 2 | Scrap(Changed Scrap) | 无限 | 已被标记为「数据变更」、待 notifyItemChanged 二次 bind 的 ViewHolder |
| 3 | Cache(RecycledViewPool 之前的 Cache) | 默认 5 | 滚动离开屏幕的 ViewHolder 进这里,复用不重新 bind(认为数据没变) |
| 4 | RecycledViewPool | 默认 5/type | 同 type 共享池,复用时必须重新 bind |
9.1 复用优先级
Item 滚动离开屏幕 → Cache(5 个)→ 满了后挤进 RecycledViewPool → 池也满了直接 GC。新 Item 出现时反过来找:先 Scrap → Cache → Pool → 实在没人能复用才 onCreateViewHolder。
这意味着只要数据布局相同,ViewHolder 不会被反复创建,避免了 findViewById 的反复 IO。这就是 RecyclerView 性能的根源。
9.2 两个调优点
setItemViewCacheSize(n):调高 Cache 容量,让回滚时不重新 bind。代价是更多内存占用- 共享 RecycledViewPool:多 Tab 同结构列表共用一个 Pool。常见于 ViewPager2 + 多个 RecyclerView 的场景
RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
rv1.setRecycledViewPool(pool);
rv2.setRecycledViewPool(pool);
rv3.setRecycledViewPool(pool);
注意不同 viewType 之间不互通,多个 Tab 列表 Item 布局必须一致才有意义。
10. 完整示例:带 Diff + 拖拽的笔记列表
把上面综合起来:一个能添加、删除、长按拖拽的列表。Activity 布局略,仅给关键代码:
package com.example.recyclerviewdemo;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private NoteAdapter adapter;
private final List<Note> data = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView rv = findViewById(R.id.recyclerView);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.addItemDecoration(new SpaceItemDecoration(8));
adapter = new NoteAdapter();
rv.setAdapter(adapter);
// 初始化数据
for (int i = 0; i < 30; i++) {
data.add(new Note("笔记 " + (i + 1), "副标题 " + (i + 1)));
}
adapter.submitList(new ArrayList<>(data)); // submitList 需要 List 的拷贝避免外层改
ItemTouchHelper.SimpleCallback cb = new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END
) {
@Override
public boolean onMove(@NonNull RecyclerView rv, @NonNull RecyclerView.ViewHolder vh,
@NonNull RecyclerView.ViewHolder target) {
int from = vh.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(data, from, to);
adapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder vh, int direction) {
int pos = vh.getAdapterPosition();
data.remove(pos);
adapter.submitList(new ArrayList<>(data));
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
};
new ItemTouchHelper(cb).attachToRecyclerView(rv);
findViewById(R.id.btnAdd).setOnClickListener(v -> {
int size = data.size() + 1;
data.add(0, new Note("新笔记 " + size, "刚刚添加"));
adapter.submitList(new ArrayList<>(data));
});
}
}
注意 submitList 传的是 new ArrayList<>(data) 拷贝,否则 DiffUtil 看到的是同一引用,会算成「无变化」。
常见坑与最佳实践
notifyDataSetChanged滥用:会跳过 DiffUtil、丢失动画、强制全部重新 bind。能用notifyItemInserted/Removed/Moved/Changed就别用notifyDataSetChanged;能用ListAdapter.submitList更好。onBindViewHolder里写耗时操作:列表滚动时这个方法会高频调用,不能在这里做 IO、网络、数据库查询。所有耗时数据应在submitList之前准备好。findViewById写在onBindViewHolder:等于放弃了 ViewHolder 的优化。所有findViewById在 ViewHolder 构造函数里做一次。- 数据修改后没同步 notify:会出现「屏幕显示的数据和 list 里不一致」的诡异 bug。改数据必须立刻
notifyXxx。 - 多类型列表的
getItemViewType返回position:这是新手高频错误,会导致每种 position 都是不同 type,缓存完全失效。Type 必须是基于「数据类型」的常量集合。 RecyclerView高度写wrap_content+ 嵌套在 ScrollView 里:会测量所有 Item,性能极差且只能显示一行高度。要么让 RecyclerView 撑满父容器,要么用 NestedScrollView 单容器思路。- DiffUtil 的
areItemsTheSame用整个对象equals:会让同内容不同实例被判定为「不同条目」,触发不必要的删除/插入动画。areItemsTheSame用 id 比较,areContentsTheSame才用内容比较。
章节小结
- RecyclerView 拆分为 Adapter / ViewHolder / LayoutManager / ItemDecoration / ItemAnimator / ItemTouchHelper,各司其职
- Adapter 的 ViewHolder 写成
static内部类,onCreateViewHolder只 inflate + findViewById,onBindViewHolder只做 setData LinearLayoutManager/GridLayoutManager/StaggeredGridLayoutManager三种布局管理器ItemDecoration.getItemOffsets给 Item 加间距,onDraw画线;网格分割线要自己写ListAdapter+DiffUtil.ItemCallback实现高效局部刷新,告别notifyDataSetChangedItemTouchHelper.SimpleCallback一行attachToRecyclerView实现长按拖拽和侧滑删除- 四级缓存:Scrap → Cache → RecycledViewPool → 销毁;同类型 Item 复用,避免反复 inflate
下一章预告
到此你已经能搭出任何「标准」界面。但有时产品要的控件系统库根本没有——圆形进度环、带波纹的按钮、特殊手势的轮盘。下一章我们走进 自定义 View 的世界:measure/layout/draw 绘制流程、onMeasure / onDraw 实战、自定义属性、事件分发机制,最后手写一个圆形进度条综合示例。