模块 06:RecyclerView 与列表渲染
目标:掌握 Android 中最核心的列表组件 RecyclerView,理解 Adapter 模式和 ViewHolder 机制。
前端对照:列表渲染
| 前端 | Android | 说明 |
|---|---|---|
items.map(item => <Card />) | RecyclerView + Adapter | 列表渲染 |
| 虚拟滚动 (react-window) | RecyclerView (内置) | 只渲染可见项 |
| 列表项组件 | ViewHolder + item 布局 | 单个列表项 |
key 属性 | getItemId() | 唯一标识 |
| 下拉刷新 | SwipeRefreshLayout | 刷新控件 |
| 无限滚动 | OnScrollListener | 加载更多 |
1. RecyclerView 核心概念
RecyclerView 是 Android 的"虚拟列表",类似前端的 react-virtualized 或 react-window。它只创建和渲染屏幕可见范围内的视图,滚动时复用已有的 ViewHolder。
三个核心组件:
| 组件 | 职责 | 前端类比 |
|---|---|---|
RecyclerView | 容器,管理滚动和回收 | 列表容器组件 |
LayoutManager | 控制排列方式 | CSS layout 属性 |
Adapter | 数据和视图的桥梁 | items.map() 的映射函数 |
2. 基础用法:完整示例
2.1 数据模型
java
public class User {
private String name;
private String email;
private String avatarUrl;
public User(String name, String email, String avatarUrl) {
this.name = name;
this.email = email;
this.avatarUrl = avatarUrl;
}
public String getName() { return name; }
public String getEmail() { return email; }
public String getAvatarUrl() { return avatarUrl; }
}2.2 列表项布局
xml
<!-- res/layout/item_user.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
</LinearLayout>
</LinearLayout>2.3 Adapter 实现
java
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHolder> {
private List<User> users;
private OnItemClickListener listener;
// 点击回调接口
public interface OnItemClickListener {
void onItemClick(User user, int position);
}
public UserAdapter(List<User> users, OnItemClickListener listener) {
this.users = users;
this.listener = listener;
}
// ① 创建 ViewHolder(类似创建列表项组件)
@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
// ② 绑定数据到 ViewHolder(类似 .map(item => render(item)))
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
User user = users.get(position);
holder.bind(user);
}
// ③ 返回数据总数
@Override
public int getItemCount() {
return users != null ? users.size() : 0;
}
// ViewHolder:持有列表项中所有子视图的引用
class UserViewHolder extends RecyclerView.ViewHolder {
private ImageView ivAvatar;
private TextView tvName;
private TextView tvEmail;
UserViewHolder(View itemView) {
super(itemView);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
tvName = itemView.findViewById(R.id.tv_name);
tvEmail = itemView.findViewById(R.id.tv_email);
// 设置点击事件
itemView.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION && listener != null) {
listener.onItemClick(users.get(position), position);
}
});
}
void bind(User user) {
tvName.setText(user.getName());
tvEmail.setText(user.getEmail());
Glide.with(itemView.getContext())
.load(user.getAvatarUrl())
.circleCrop()
.into(ivAvatar);
}
}
}2.4 在 Activity/Fragment 中使用
java
public class UserListActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private UserAdapter adapter;
private List<User> userList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
recyclerView = findViewById(R.id.rv_users);
// 1. 设置 LayoutManager(决定排列方式)
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 2. 创建 Adapter
adapter = new UserAdapter(userList, (user, position) -> {
// 处理点击
Intent intent = new Intent(this, UserDetailActivity.class);
intent.putExtra("user_name", user.getName());
startActivity(intent);
});
// 3. 设置 Adapter
recyclerView.setAdapter(adapter);
// 4. 加载数据
loadUsers();
}
private void loadUsers() {
// 模拟网络请求
userList.add(new User("张三", "zhangsan@example.com", "https://..."));
userList.add(new User("李四", "lisi@example.com", "https://..."));
userList.add(new User("王五", "wangwu@example.com", "https://..."));
// 通知 Adapter 数据已变化(类似 setState 触发重新渲染)
adapter.notifyDataSetChanged();
}
}2.5 布局文件
xml
<!-- res/layout/activity_user_list.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="用户列表" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_users"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp" />
</LinearLayout>3. LayoutManager 类型
java
// 垂直列表(最常用)
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 水平列表
LinearLayoutManager hlm = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(hlm);
// 网格布局(类似 CSS Grid / display: grid)
recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); // 2列
// 瀑布流(类似 Pinterest 布局)
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));4. 数据更新方法
java
// 全量刷新(简单但低效)
adapter.notifyDataSetChanged();
// 精确更新(推荐,带动画效果)
// 插入一项
users.add(0, newUser);
adapter.notifyItemInserted(0);
// 删除一项
users.remove(position);
adapter.notifyItemRemoved(position);
// 修改一项
users.set(position, updatedUser);
adapter.notifyItemChanged(position);
// 移动一项
Collections.swap(users, fromPos, toPos);
adapter.notifyItemMoved(fromPos, toPos);
// 使用 DiffUtil(推荐,自动计算差异)
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() { return oldList.size(); }
@Override
public int getNewListSize() { return newList.size(); }
@Override
public boolean areItemsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).getId() == newList.get(newPos).getId();
}
@Override
public boolean areContentsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).equals(newList.get(newPos));
}
});
result.dispatchUpdatesTo(adapter);推荐使用 ListAdapter(内置 DiffUtil):
java
public class UserListAdapter extends ListAdapter<User, UserListAdapter.UserViewHolder> {
private static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(User oldItem, User newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(User oldItem, User newItem) {
return oldItem.getName().equals(newItem.getName())
&& oldItem.getEmail().equals(newItem.getEmail());
}
};
public UserListAdapter() {
super(DIFF_CALLBACK);
}
@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
holder.bind(getItem(position));
}
// ... ViewHolder 同上
}
// 使用
UserListAdapter adapter = new UserListAdapter();
recyclerView.setAdapter(adapter);
// 更新数据(自动计算差异并动画)
adapter.submitList(newUserList);5. 多类型列表
java
public class MultiTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private static final int TYPE_FOOTER = 2;
@Override
public int getItemViewType(int position) {
if (position == 0) return TYPE_HEADER;
if (position == getItemCount() - 1) return TYPE_FOOTER;
return TYPE_ITEM;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_HEADER:
View headerView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_header, parent, false);
return new HeaderViewHolder(headerView);
case TYPE_FOOTER:
View footerView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(footerView);
default:
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new ItemViewHolder(itemView);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HeaderViewHolder) {
((HeaderViewHolder) holder).bind();
} else if (holder instanceof ItemViewHolder) {
((ItemViewHolder) holder).bind(items.get(position - 1));
} else if (holder instanceof FooterViewHolder) {
((FooterViewHolder) holder).bind();
}
}
}6. 下拉刷新 + 上拉加载更多
xml
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>java
SwipeRefreshLayout swipeRefresh = findViewById(R.id.swipe_refresh);
swipeRefresh.setOnRefreshListener(() -> {
// 下拉刷新
loadData(true);
});
// 上拉加载更多
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
int totalItemCount = lm.getItemCount();
int lastVisibleItem = lm.findLastVisibleItemPosition();
// 滑到倒数第 3 项时触发加载更多
if (totalItemCount <= lastVisibleItem + 3 && !isLoading) {
loadMore();
}
}
});
private void loadData(boolean isRefresh) {
if (isRefresh) {
page = 1;
}
// 网络请求...
api.getUsers(page, new Callback() {
@Override
public void onSuccess(List<User> newUsers) {
if (isRefresh) {
userList.clear();
userList.addAll(newUsers);
adapter.notifyDataSetChanged();
} else {
int startPos = userList.size();
userList.addAll(newUsers);
adapter.notifyItemRangeInserted(startPos, newUsers.size());
}
swipeRefresh.setRefreshing(false);
isLoading = false;
}
});
}7. 添加分割线和装饰
java
// 添加默认分割线
recyclerView.addItemDecoration(
new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
// 自定义间距装饰
public classSpaceItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpaceItemDecoration(int space) {
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = space;
}
}
}
recyclerView.addItemDecoration(new SpaceItemDecoration(16));8. ItemTouchHelper:滑动和拖拽
java
// 左滑删除 + 长按拖拽排序
ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN, // 拖拽方向
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT // 滑动方向
) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
// 拖拽排序
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(userList, from, to);
adapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 滑动删除
int position = viewHolder.getAdapterPosition();
userList.remove(position);
adapter.notifyItemRemoved(position);
}
});
helper.attachToRecyclerView(recyclerView);9. 老项目中的 ListView + BaseAdapter 模式
老项目中大量使用 ListView 和 BaseAdapter,它是 RecyclerView 的前身。理解它有助于阅读老代码:
java
// ListView 时代的适配器(类似 RecyclerView.Adapter 的祖先)
// 来自实际教程项目 PlanetListAdapter.java
public class PlanetListAdapter extends BaseAdapter
implements AdapterView.OnItemClickListener {
private Context mContext;
private List<Planet> mPlanetList;
public PlanetListAdapter(Context context, List<Planet> planetList) {
mContext = context;
mPlanetList = planetList;
}
public int getCount() { return mPlanetList.size(); }
public Object getItem(int pos) { return mPlanetList.get(pos); }
public long getItemId(int pos) { return pos; }
// ⭐ getView = RecyclerView 的 onCreateViewHolder + onBindViewHolder 合并体
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) { // 转换视图为空,需要创建
holder = new ViewHolder();
// 根据布局文件生成视图(类似 RecyclerView 的 inflate)
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, null);
holder.ivIcon = convertView.findViewById(R.id.iv_icon);
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.tvDesc = convertView.findViewById(R.id.tv_desc);
convertView.setTag(holder); // 将 ViewHolder 保存到 convertView
} else {
holder = (ViewHolder) convertView.getTag(); // 复用已有 ViewHolder
}
// 绑定数据(类似 onBindViewHolder)
Planet planet = mPlanetList.get(position);
holder.ivIcon.setImageResource(planet.image);
holder.tvName.setText(planet.name);
holder.tvDesc.setText(planet.desc);
return convertView;
}
// ViewHolder 模式:缓存列表项的子视图引用,避免重复 findViewById
public final class ViewHolder {
public ImageView ivIcon;
public TextView tvName;
public TextView tvDesc;
}
// 处理列表项点击(类似 RecyclerView 的 OnItemClickListener)
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(mContext, "点击了" + mPlanetList.get(position).name,
Toast.LENGTH_LONG).show();
}
}java
// 在 Activity 中使用 ListView(老项目写法)
ListView listView = findViewById(R.id.lv_planet);
PlanetListAdapter adapter = new PlanetListAdapter(this, planetList);
listView.setAdapter(adapter); // 设置适配器
listView.setOnItemClickListener(adapter); // 设置点击监听ListView vs RecyclerView 对比:
| 特性 | ListView (老) | RecyclerView (新) |
|---|---|---|
| ViewHolder | 手动实现,可选 | 强制使用 |
| 布局方式 | 只能垂直列表 | LayoutManager 灵活控制 |
| 动画 | 无内置 | 内置增删改动画 |
| 局部刷新 | 不支持 | notifyItemXxx() |
| 点击事件 | setOnItemClickListener | ViewHolder 中手动设置 |
老项目迁移提示:如果你看到
extends BaseAdapter或extends ArrayAdapter,这就是 ListView 时代的适配器。迁移到 RecyclerView 时,需要改为extends RecyclerView.Adapter并拆分getView()为onCreateViewHolder()+onBindViewHolder()。
前端开发者备忘
| 前端列表概念 | Android RecyclerView | 备注 |
|---|---|---|
items.map(render) | onBindViewHolder() | 数据到视图的映射 |
| 列表项组件 | ViewHolder | 复用单元 |
key prop | getItemId() + setHasStableIds(true) | 唯一标识 |
| 虚拟滚动 | 内置(RecyclerView 本身就是虚拟列表) | 性能优化 |
| 列表容器 | RecyclerView | 外层容器 |
display: grid | GridLayoutManager | 网格布局 |
| 下拉刷新 | SwipeRefreshLayout | 刷新容器 |
| 无限滚动 | OnScrollListener | 滚动监听 |
setState(newItems) | adapter.notifyDataSetChanged() / submitList() | 数据更新 |
| 拖拽排序 | ItemTouchHelper | 手势交互 |
下一步
掌握了列表渲染后,接下来学习 模块 07:网络请求与数据持久化,学习如何获取和存储数据。