9、ListView与RecyclerView:Adapter适配器模式、ViewHolder优化、RecyclerView的LayoutManager与ItemDecoration
列表,是移动应用中最常见的界面形态。从通讯录到朋友圈,从商品列表到设置页面,几乎每个App都离不开它。在Android世界里,我们经历了从ListView到RecyclerView的演进。说实话,我刚入行那会儿,还在用ListView写各种列表,后来RecyclerView一出来,我就知道——时代变了。
这一章,我们来聊聊列表控件的核心机制。我会把Adapter适配器模式、ViewHolder优化、LayoutManager和ItemDecoration这些知识点串起来讲。你想想看,理解了这些,你就能写出流畅得像丝绸一样的列表了。
9.1 从ListView到RecyclerView:为什么要有这个进化?
ListView是Android早期就有的列表控件。它很经典,但问题也不少。我在项目中遇到过最头疼的事:ListView的复用机制不够灵活,而且没有强制使用ViewHolder。很多新手直接在里面new View,结果一滑就卡成PPT。
RecyclerView的出现,说白了就是Google把ListView的痛点全给治了。它更灵活,更高效,而且设计上更符合「单一职责」原则。
| 对比维度 | ListView | RecyclerView |
|---|---|---|
| 布局管理 | 只有垂直列表 | 支持线性、网格、瀑布流等 |
| ViewHolder | 可选(不强制) | 强制使用(通过Adapter实现) |
| 动画支持 | 需要自己写 | 内置ItemAnimator |
| 分割线 | 通过属性或自定义 | 通过ItemDecoration |
| 点击事件 | 有OnItemClickListener | 需要自己实现(但更灵活) |
嗯,这里要注意:RecyclerView并不是ListView的简单替代品,而是一种全新的设计思路。它把「怎么布局」、「怎么装饰」、「怎么动画」这些事都拆开了,让你可以自由组合。
9.2 Adapter适配器模式:数据与视图的桥梁
Adapter这个词,直译就是「适配器」。它的作用很简单:把数据适配成视图。你有一堆Java对象,Adapter负责把它们变成屏幕上一个个看得见摸得着的Item。
我个人习惯把Adapter理解成一个「翻译官」。数据说「我是User对象,有名字和头像」,视图说「我需要一个TextView和一个ImageView」。Adapter就在中间做翻译和组装的工作。
核心要点:Adapter模式的核心是解耦。数据层不关心视图怎么展示,视图层也不关心数据从哪里来。Adapter是它们之间的契约。
来看一个标准的RecyclerView.Adapter实现:
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHolder> {
private List<User> userList;
public UserAdapter(List<User> userList) {
this.userList = userList;
}
@NonNull
@Override
public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 创建ViewHolder,加载布局
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
// 绑定数据到ViewHolder
User user = userList.get(position);
holder.tvName.setText(user.getName());
holder.ivAvatar.setImageResource(user.getAvatarResId());
}
@Override
public int getItemCount() {
return userList == null ? 0 : userList.size();
}
static class UserViewHolder extends RecyclerView.ViewHolder {
TextView tvName;
ImageView ivAvatar;
public UserViewHolder(@NonNull View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
ivAvatar = itemView.findViewById(R.id.iv_avatar);
}
}
}
这段代码里,有三个方法你必须掌握:
- onCreateViewHolder:创建ViewHolder实例。注意,这个方法只在需要新视图时调用,复用的视图不会走这里。
- onBindViewHolder:把数据绑定到ViewHolder上。这是最频繁调用的方法,性能优化主要看这里。
- getItemCount:告诉RecyclerView一共有多少条数据。
小技巧:在onBindViewHolder里,尽量避免做耗时操作,比如加载大图、解析JSON。我曾经在一个列表里直接做图片压缩,结果滑动时卡得不行。后来改成异步加载,瞬间流畅了。
9.3 ViewHolder优化:为什么它能救命?
ViewHolder,说白了就是一个「视图缓存容器」。它的核心思想是:避免重复调用findViewById。
你想想看,如果列表有1000条数据,每次滑动到新的Item都要findViewById一次,那得有多慢?findViewById的本质是遍历View树,这在布局复杂时开销很大。
ViewHolder的做法是:在创建视图时,一次性把所有的子View引用都存起来。后面再绑定数据时,直接拿引用用就行了,不用再找一遍。
性能对比:使用ViewHolder的列表,滑动帧率可以稳定在55-60fps。不使用的话,可能掉到30fps以下。差距就是这么明显。
我曾经接手过一个老项目,里面ListView的Adapter完全没有ViewHolder。每次getView都new ViewHolder,然后findViewById。我改完之后,列表滑动流畅度直接翻倍。嗯,这就是优化的力量。
在RecyclerView中,ViewHolder是强制使用的。你继承RecyclerView.ViewHolder,然后在构造函数里把子View都找好。这是Google帮你做的「强制规范」,也是RecyclerView比ListView高效的原因之一。
9.4 RecyclerView的LayoutManager:布局的指挥官
LayoutManager是RecyclerView的灵魂。它决定了Item在屏幕上怎么排列。Google提供了三种内置的LayoutManager:
- LinearLayoutManager:线性排列,可以垂直也可以水平。
- GridLayoutManager:网格排列,像九宫格那样。
- StaggeredGridLayoutManager:瀑布流排列,每个Item高度可以不一样。
我个人习惯把LayoutManager比作「指挥官」。它告诉RecyclerView:「第一个Item放左上角,第二个放它右边,第三个放下面...」所有Item的位置、滚动行为,都由它决定。
来看一个简单的设置:
// 垂直列表
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
// 2列网格
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2);
recyclerView.setLayoutManager(gridLayoutManager);
// 瀑布流,3列
StaggeredGridLayoutManager staggeredLayoutManager =
new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(staggeredLayoutManager);
注意:LayoutManager必须在设置Adapter之前设置,或者至少要在设置Adapter之前确定。否则RecyclerView无法正确测量和布局Item。我见过有人把顺序搞反了,结果列表显示空白,排查了半天。
你还可以自定义LayoutManager。比如做一个环形布局、一个扇形布局。不过说实话,大部分场景下内置的三种就够用了。自定义LayoutManager的坑很多,我建议你除非有特殊需求,否则别轻易尝试。
9.5 ItemDecoration:给列表加点「装饰」
ItemDecoration,直译是「Item装饰器」。它的作用是在Item的绘制过程中,额外画一些东西。最常见的用途就是画分割线。
在ListView时代,分割线是通过android:divider属性设置的。但在RecyclerView里,分割线完全由ItemDecoration控制。这样做的好处是:你可以画出任意样式的分割线,甚至可以在Item之间画图标、画颜色块。
来看一个简单的分割线实现:
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable divider;
private int orientation;
public DividerItemDecoration(Context context, int orientation) {
// 从主题中获取默认的分割线drawable
int[] attrs = new int[]{android.R.attr.listDivider};
TypedArray ta = context.obtainStyledAttributes(attrs);
divider = ta.getDrawable(0);
ta.recycle();
this.orientation = orientation;
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
// 在Item绘制之后,绘制分割线
if (orientation == LinearLayoutManager.VERTICAL) {
drawVertical(c, parent);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
// 为每个Item预留分割线的空间
if (orientation == LinearLayoutManager.VERTICAL) {
outRect.set(0, 0, 0, divider.getIntrinsicHeight());
}
}
private void drawVertical(Canvas c, RecyclerView parent) {
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params =
(RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getBottom() + params.bottomMargin;
int bottom = top + divider.getIntrinsicHeight();
divider.setBounds(left, top, right, bottom);
divider.draw(c);
}
}
}
这段代码里有两个关键方法:
- getItemOffsets:告诉RecyclerView,每个Item需要预留多少空间。这个空间就是用来画分割线的。
- onDraw:在Item绘制完成后,在预留的空间里画分割线。
避坑指南:我曾经在ItemDecoration里直接修改了Item的布局参数,结果导致Item显示错乱。记住,ItemDecoration只负责「画」,不要动Item本身的位置和大小。getItemOffsets只是预留空间,不是修改Item。
你还可以用ItemDecoration做更多事:比如在第一个Item上面画一个Header,或者在最后一个Item下面画一个Footer。甚至可以在Item之间画一条渐变色分割线。只要你想得到,ItemDecoration都能画出来。
9.6 知识体系总览
为了让你更直观地理解这一章的知识结构,我画了一张图:
从这张图你可以看到,RecyclerView相比ListView,最大的优势就是「模块化」。每个组件各司其职,你可以像搭积木一样组合它们。想换布局?换LayoutManager就行。想加分割线?加ItemDecoration就行。想加动画?加ItemAnimator就行。
这种设计思路,说白了就是「组合优于继承」。RecyclerView没有把所有的功能都塞到一个类里,而是拆成了多个可插拔的组件。这也是现代Android框架的设计趋势。
9.7 实战建议与避坑指南
最后,我结合自己的经验,给你几条实战建议:
- 永远使用ViewHolder。不管是用ListView还是RecyclerView,ViewHolder都是必须的。这是性能优化的第一道防线。
- 合理选择LayoutManager。不要什么列表都用LinearLayoutManager。如果是图片展示,GridLayoutManager更合适。如果是笔记类App,StaggeredGridLayoutManager更有感觉。
- ItemDecoration不要画太复杂。onDraw方法是在UI线程执行的,画太多东西会影响滑动流畅度。如果一定要画复杂的装饰,考虑用自定义View替代。
- 注意数据更新方式。RecyclerView提供了notifyItemChanged、notifyItemInserted等增量更新方法,比notifyDataSetChanged高效得多。我建议你尽量用增量更新。
- 列表嵌套要谨慎。RecyclerView嵌套RecyclerView,或者RecyclerView嵌套ScrollView,很容易出现滑动冲突和性能问题。能避免就避免。
曾经踩过的坑:我在一个项目中,用RecyclerView嵌套了多个RecyclerView,结果滑动时卡得不行。后来发现是内层RecyclerView的高度没有正确测量。解决方案是用NestedScrollView配合RecyclerView,或者干脆把内层列表改成LinearLayout手动添加View。嗯,从那以后我再也不敢随便嵌套了。
好了,这一章的内容就到这里。列表控件是Android开发的基础,但基础不代表简单。Adapter模式、ViewHolder优化、LayoutManager和ItemDecoration,每一个都值得你花时间去深入理解。希望你能在实际项目中用好它们,写出流畅、优雅的列表。
公众号:蓝海资料掘金营,微信deep3321