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 知识体系总览

为了让你更直观地理解这一章的知识结构,我画了一张图:

ListView与RecyclerView知识体系 列表控件核心 ListView(经典方案) Adapter + ViewHolder(可选) 缺点:复用不灵活,功能单一 RecyclerView(现代方案) Adapter(强制ViewHolder) LayoutManager(布局指挥官) ItemDecoration(装饰器) ItemAnimator(动画支持) 核心思想:解耦、复用、灵活组合

从这张图你可以看到,RecyclerView相比ListView,最大的优势就是「模块化」。每个组件各司其职,你可以像搭积木一样组合它们。想换布局?换LayoutManager就行。想加分割线?加ItemDecoration就行。想加动画?加ItemAnimator就行。

这种设计思路,说白了就是「组合优于继承」。RecyclerView没有把所有的功能都塞到一个类里,而是拆成了多个可插拔的组件。这也是现代Android框架的设计趋势。

9.7 实战建议与避坑指南

最后,我结合自己的经验,给你几条实战建议:

  1. 永远使用ViewHolder。不管是用ListView还是RecyclerView,ViewHolder都是必须的。这是性能优化的第一道防线。
  2. 合理选择LayoutManager。不要什么列表都用LinearLayoutManager。如果是图片展示,GridLayoutManager更合适。如果是笔记类App,StaggeredGridLayoutManager更有感觉。
  3. ItemDecoration不要画太复杂。onDraw方法是在UI线程执行的,画太多东西会影响滑动流畅度。如果一定要画复杂的装饰,考虑用自定义View替代。
  4. 注意数据更新方式。RecyclerView提供了notifyItemChanged、notifyItemInserted等增量更新方法,比notifyDataSetChanged高效得多。我建议你尽量用增量更新。
  5. 列表嵌套要谨慎。RecyclerView嵌套RecyclerView,或者RecyclerView嵌套ScrollView,很容易出现滑动冲突和性能问题。能避免就避免。

曾经踩过的坑:我在一个项目中,用RecyclerView嵌套了多个RecyclerView,结果滑动时卡得不行。后来发现是内层RecyclerView的高度没有正确测量。解决方案是用NestedScrollView配合RecyclerView,或者干脆把内层列表改成LinearLayout手动添加View。嗯,从那以后我再也不敢随便嵌套了。

好了,这一章的内容就到这里。列表控件是Android开发的基础,但基础不代表简单。Adapter模式、ViewHolder优化、LayoutManager和ItemDecoration,每一个都值得你花时间去深入理解。希望你能在实际项目中用好它们,写出流畅、优雅的列表。


公众号:蓝海资料掘金营,微信deep3321