文章目录
- RecyclerView多布局详解
-
- [1. 引入:为什么需要多布局?](#1. 引入:为什么需要多布局?)
-
- [1.1 单一布局的局限](#1.1 单一布局的局限)
- [1.2 常见多布局场景](#1.2 常见多布局场景)
- [2. 多布局的核心:getItemViewType](#2. 多布局的核心:getItemViewType)
-
- [2.1 原理](#2.1 原理)
- [2.2 重写getItemViewType](#2.2 重写getItemViewType)
- [3. 完整实现:聊天列表多布局](#3. 完整实现:聊天列表多布局)
-
- [3.1 定义布局类型常量](#3.1 定义布局类型常量)
- [3.2 重写getItemViewType](#3.2 重写getItemViewType)
- [3.3 创建不同的ViewHolder](#3.3 创建不同的ViewHolder)
- [3.4 根据类型创建对应的ViewHolder](#3.4 根据类型创建对应的ViewHolder)
- [3.5 根据ViewHolder类型绑定数据](#3.5 根据ViewHolder类型绑定数据)
- [3.6 返回Item总数](#3.6 返回Item总数)
- [4. 三种布局的XML](#4. 三种布局的XML)
-
- [4.1 自己发的消息(item_chat_self.xml)](#4.1 自己发的消息(item_chat_self.xml))
- [4.2 别人发的消息(item_chat_other.xml)](#4.2 别人发的消息(item_chat_other.xml))
- [4.3 时间戳(item_chat_time.xml)](#4.3 时间戳(item_chat_time.xml))
- [5. 多布局的优化技巧](#5. 多布局的优化技巧)
-
- [5.1 用SparseArray管理布局ID](#5.1 用SparseArray管理布局ID)
- [5.2 避免在onBindViewHolder中做耗时操作](#5.2 避免在onBindViewHolder中做耗时操作)
- [5.3 多布局的局部刷新](#5.3 多布局的局部刷新)
- [6. 多布局与缓存的关系](#6. 多布局与缓存的关系)
-
- [6.1 ViewType相同才能复用](#6.1 ViewType相同才能复用)
- [6.2 缓存池的容量](#6.2 缓存池的容量)
- [7. 易错点总结](#7. 易错点总结)
- [8. 总结](#8. 总结)
RecyclerView多布局详解
1. 引入:为什么需要多布局?
1.1 单一布局的局限
普通的RecyclerView列表,每个Item长一个样:
┌─────────────────────┐
│ 头像 用户名 内容 │
├─────────────────────┤
│ 头像 用户名 内容 │
├─────────────────────┤
│ 头像 用户名 内容 │
└─────────────────────┘
但实际开发中,列表往往需要多种样式的Item:
┌─────────────────────┐
│ 公告:系统维护通知 │ ← 公告布局
├─────────────────────┤
│ 头像 张三 你好 │ ← 聊天布局
├─────────────────────┤
│ 头像 李四 在吗 │ ← 聊天布局
├─────────────────────┤
│ 2026年9月19日 │ ← 时间戳布局
├─────────────────────┤
│ 头像 王五 晚上好 │ ← 聊天布局
└─────────────────────┘
多布局就是让RecyclerView根据不同的数据类型,显示不同的Item样式。
1.2 常见多布局场景
| 场景 | 多布局需求 |
|---|---|
| 聊天列表 | 自己发的消息(右)、别人发的消息(左)、时间戳(中) |
| 电商首页 | 轮播图、分类入口、商品列表、广告位 |
| 朋友圈 | 纯文字、带图、带视频、广告 |
| 设置页 | 普通选项、开关选项、分组标题 |
| 新闻列表 | 大图新闻、小图新闻、纯文字新闻 |
2. 多布局的核心:getItemViewType
2.1 原理
RecyclerView通过getItemViewType()方法来判断每个位置的Item应该用哪种布局:
position 0 → viewType 0 → 加载布局A
position 1 → viewType 1 → 加载布局B
position 2 → viewType 0 → 加载布局A
position 3 → viewType 2 → 加载布局C
2.2 重写getItemViewType
java
@Override
public int getItemViewType(int position) {
// 根据position返回对应的类型
if (position == 0) {
return TYPE_HEADER; // 头部
} else if (dataList.get(position).getType() == MSG_SELF) {
return TYPE_SELF; // 自己发的消息
} else if (dataList.get(position).getType() == MSG_OTHER) {
return TYPE_OTHER; // 别人发的消息
} else {
return TYPE_TIME; // 时间戳
}
}
3. 完整实现:聊天列表多布局
3.1 定义布局类型常量
java
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
// 定义三种布局类型
public static final int TYPE_SELF = 0; // 自己发的消息
public static final int TYPE_OTHER = 1; // 别人发的消息
public static final int TYPE_TIME = 2; // 时间戳
private List<ChatMessage> messageList;
private Context context;
public ChatAdapter(Context context, List<ChatMessage> messageList) {
this.context = context;
this.messageList = messageList;
}
}
3.2 重写getItemViewType
java
@Override
public int getItemViewType(int position) {
ChatMessage message = messageList.get(position);
if (message.isTime()) {
return TYPE_TIME; // 时间戳类型
} else if (message.isSelf()) {
return TYPE_SELF; // 自己发的
} else {
return TYPE_OTHER; // 别人发的
}
}
3.3 创建不同的ViewHolder
java
// 自己发的消息
static class SelfViewHolder extends RecyclerView.ViewHolder {
TextView tvContent;
ImageView ivAvatar;
public SelfViewHolder(View itemView) {
super(itemView);
tvContent = itemView.findViewById(R.id.tv_content);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
}
}
// 别人发的消息
static class OtherViewHolder extends RecyclerView.ViewHolder {
TextView tvContent;
TextView tvName;
ImageView ivAvatar;
public OtherViewHolder(View itemView) {
super(itemView);
tvContent = itemView.findViewById(R.id.tv_content);
tvName = itemView.findViewById(R.id.tv_name);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
}
}
// 时间戳
static class TimeViewHolder extends RecyclerView.ViewHolder {
TextView tvTime;
public TimeViewHolder(View itemView) {
super(itemView);
tvTime = itemView.findViewById(R.id.tv_time);
}
}
3.4 根据类型创建对应的ViewHolder
java
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
switch (viewType) {
case TYPE_SELF:
// 加载自己消息的布局
View selfView = inflater.inflate(R.layout.item_chat_self, parent, false);
return new SelfViewHolder(selfView);
case TYPE_OTHER:
// 加载别人消息的布局
View otherView = inflater.inflate(R.layout.item_chat_other, parent, false);
return new OtherViewHolder(otherView);
case TYPE_TIME:
// 加载时间戳的布局
View timeView = inflater.inflate(R.layout.item_chat_time, parent, false);
return new TimeViewHolder(timeView);
default:
throw new IllegalArgumentException("未知的viewType: " + viewType);
}
}
3.5 根据ViewHolder类型绑定数据
java
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ChatMessage message = messageList.get(position);
if (holder instanceof SelfViewHolder) {
// 绑定自己消息的数据
SelfViewHolder selfHolder = (SelfViewHolder) holder;
selfHolder.tvContent.setText(message.getContent());
// 加载头像...
} else if (holder instanceof OtherViewHolder) {
// 绑定别人消息的数据
OtherViewHolder otherHolder = (OtherViewHolder) holder;
otherHolder.tvContent.setText(message.getContent());
otherHolder.tvName.setText(message.getSenderName());
// 加载头像...
} else if (holder instanceof TimeViewHolder) {
// 绑定时间戳的数据
TimeViewHolder timeHolder = (TimeViewHolder) holder;
timeHolder.tvTime.setText(message.getTime());
}
}
3.6 返回Item总数
java
@Override
public int getItemCount() {
return messageList.size();
}
4. 三种布局的XML
4.1 自己发的消息(item_chat_self.xml)
xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:padding="10dp">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_bubble_green"
android:padding="10dp"
android:textColor="#FFFFFF"
android:maxWidth="240dp" />
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="8dp"
android:src="@drawable/avatar_self" />
</LinearLayout>
4.2 别人发的消息(item_chat_other.xml)
xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/avatar_other" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="8dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#999999" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_bubble_white"
android:padding="10dp"
android:maxWidth="240dp" />
</LinearLayout>
</LinearLayout>
4.3 时间戳(item_chat_time.xml)
xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp">
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#E0E0E0"
android:padding="4dp 8dp"
android:textSize="12sp"
android:textColor="#666666" />
</LinearLayout>
5. 多布局的优化技巧
5.1 用SparseArray管理布局ID
如果布局类型很多,用switch-case写起来很臃肿,可以用SparseArray优化:
java
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
// 用SparseArray存布局ID
private SparseArray<Integer> layoutIds = new SparseArray<>();
public ChatAdapter() {
layoutIds.put(TYPE_SELF, R.layout.item_chat_self);
layoutIds.put(TYPE_OTHER, R.layout.item_chat_other);
layoutIds.put(TYPE_TIME, R.layout.item_chat_time);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 直接从SparseArray拿布局ID
int layoutId = layoutIds.get(viewType);
View view = LayoutInflater.from(context).inflate(layoutId, parent, false);
// 根据viewType创建对应的ViewHolder
if (viewType == TYPE_SELF) return new SelfViewHolder(view);
if (viewType == TYPE_OTHER) return new OtherViewHolder(view);
return new TimeViewHolder(view);
}
}
5.2 避免在onBindViewHolder中做耗时操作
java
// 错误:每次绑定都加载图片
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Glide.with(context).load(url).into(holder.ivAvatar); // 每次都加载
}
// 正确:用图片加载框架自动处理缓存
// Glide/Coil 自带缓存机制,但注意不要在onBind中做复杂计算
5.3 多布局的局部刷新
java
// 只刷新指定类型的Item
public void updateTimeItem(int position) {
notifyItemChanged(position, PAYLOAD_TIME);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
// 全量刷新
onBindViewHolder(holder, position);
} else {
// 局部刷新
for (Object payload : payloads) {
if (PAYLOAD_TIME.equals(payload)) {
// 只更新时间戳
}
}
}
}
6. 多布局与缓存的关系
6.1 ViewType相同才能复用
ViewType 0 的Item → 只能复用 ViewType 0 的缓存
ViewType 1 的Item → 只能复用 ViewType 1 的缓存
ViewType 2 的Item → 只能复用 ViewType 2 的缓存
四级缓存中RecycledViewPool按ViewType分类存放,不同类型的View不能交叉复用。
6.2 缓存池的容量
java
// 每种ViewType默认缓存5个
pool.setMaxRecycledViews(TYPE_SELF, 10); // 自己消息缓存10个
pool.setMaxRecycledViews(TYPE_OTHER, 10); // 别人消息缓存10个
pool.setMaxRecycledViews(TYPE_TIME, 3); // 时间戳缓存3个就够了
7. 易错点总结
| 易错点 | 说明 |
|---|---|
| getItemViewType返回错误类型 | 导致onCreateViewHolder加载错误的布局 |
| onCreateViewHolder的switch漏了default | 未知类型时崩溃 |
| onBindViewHolder用错ViewHolder类型 | instanceof判断错误导致空指针 |
| 相同ViewType的布局不一致 | 同一个ViewType必须用同一个布局 |
| 忘记设置LayoutManager | RecyclerView不显示任何内容 |
8. 总结
| 核心步骤 | 说明 |
|---|---|
| 1. 定义ViewType常量 | 每种布局一个int类型 |
| 2. 重写getItemViewType | 根据position返回类型 |
| 3. 重写onCreateViewHolder | 根据类型加载不同布局 |
| 4. 重写onBindViewHolder | 根据ViewHolder类型绑定数据 |
一句话总结 :多布局的核心就是getItemViewType()返回不同值,onCreateViewHolder()根据值加载不同布局,onBindViewHolder()根据ViewHolder类型绑定不同数据。缓存池按ViewType分类缓存,同类型才能复用。