Android RecyclerView 实战指南:从入门到封神,覆盖 90% 开发场景

目录

前言:

一、为什么 RecyclerView 能取代 ListView?核心优势拆解

1. 布局灵活性:一套组件适配所有列表样式

2. 性能碾压:三级缓存机制详解

3. 功能扩展性:自带动画与交互支持

4. 架构解耦:职责分明更易维护

二、RecyclerView 基础入门:3 步实现一个标准列表

1. 第一步:配置依赖与布局文件

(1)引入 RecyclerView 依赖

(2)创建主布局(activity_main.xml)

(3)创建 Item 布局(item_user.xml)

2. 第二步:创建数据模型与 ViewHolder

(1)数据模型(UserBean.java)

(2)创建 ViewHolder

3. 第三步:实现 Adapter 与 Activity 绑定

(1)实现 Adapter(UserAdapter.java)

(2)在 Activity 中初始化 RecyclerView

三、RecyclerView 进阶技巧:覆盖 80% 开发场景

1. 多类型布局:一个列表展示不同样式 Item

实战场景:聊天列表(文字消息 + 图片消息)

(1)定义消息类型常量与数据模型

(2)创建两种 ViewHolder

(3)实现多类型 Adapter

2. 下拉刷新与上拉加载:实现无限滚动列表

(1)布局文件中添加 SwipeRefreshLayout

(2)在 Activity 中实现下拉刷新逻辑

(3)Adapter 中添加 Footer 处理

3. Item 动画:让列表交互更流畅

(1)使用默认动画

(2)自定义 Item 动画(示例:淡入淡出效果)

4. 点击事件与长按事件:优雅实现交互

(1)在 Adapter 中定义点击事件接口

(2)在 Activity 中设置监听

四、RecyclerView 性能优化:从流畅到极致

1. 优化数据更新:用 DiffUtil 替代 notifyDataSetChanged

(1)实现 DiffUtil.Callback

(2)在 Adapter 中使用 DiffUtil

2. 布局优化:减少层级与过度绘制

(1)简化 item 布局

(2)设置 RecyclerView 的 ItemViewCacheSize

(3)关闭过度绘制

3. 图片加载优化:避免 OOM 与卡顿

4. 避免在 onBindViewHolder 中做耗时操作

五、实战案例:打造电商 App 商品列表(支持布局切换)

1. 功能需求

2. 核心代码实现

(1)布局文件(activity_goods_list.xml)

(2)两种 Item 布局

(3)Adapter 实现(GoodsAdapter.java)

(4)Activity 实现(GoodsListActivity.java)

3. 效果演示

六、实战案例:订单列表(展开 / 收起 + 嵌套 RecyclerView)

1. 核心思路

2. 关键代码

(1)订单数据模型(OrderBean.java)

(2)外层 Adapter(OrderAdapter.java)

(3)内层 Adapter(OrderGoodsAdapter.java)

3. 注意事项

七、RecyclerView 常见问题与避坑指南

1. 数据错位问题

原因:

解决方案:

2. 滑动卡顿问题

原因:

解决方案:

3. 布局显示异常(item 不显示 / 显示不全)

原因:

解决方案:

4. 点击事件失效

原因:

解决方案:

八、总结与扩展

扩展学习方向


class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

前言:

作为 Android 开发的 “列表之王”,RecyclerView 凭借灵活的布局、高效的性能,成为了几乎所有 App 的必备组件。不管是电商 App 的商品列表、社交 App 的消息流,还是工具类 App 的设置页面,都能看到它的身影。但很多开发者只停留在 “能用” 层面,对其缓存机制、性能优化、复杂场景适配一知半解。这篇文章从基础用法到进阶技巧,再到实战案例,用最通俗的语言 + 可直接运行的代码,带你彻底吃透 RecyclerView,让你在开发中少踩 90% 的坑!

一、为什么 RecyclerView 能取代 ListView?核心优势拆解

在 RecyclerView 出现之前,ListView 是 Android 列表展示的首选,但它的局限性越来越难满足复杂需求。RecyclerView 的诞生,正是为了解决这些痛点:

1. 布局灵活性:一套组件适配所有列表样式

ListView 只能实现垂直线性布局,而 RecyclerView 通过LayoutManager机制,轻松支持多种布局:

  • 线性布局(垂直 / 水平):对应LinearLayoutManager,替代 ListView 的核心功能
  • 网格布局:对应GridLayoutManager,实现类似 GridView 的效果,还能动态调整列数
  • 瀑布流布局:对应StaggeredGridLayoutManager,适用于商品展示、图片流等场景
  • 自定义布局:通过继承RecyclerView.LayoutManager,实现任意复杂的列表排列

2. 性能碾压:三级缓存机制详解

RecyclerView 的性能优势核心在于其精密的三级缓存体系,比 ListView 的单一缓存高效得多:

  • Scrap 缓存:用于屏幕上可见或即将重新布局的视图,复用时无需重新绑定数据,实现 “零开销” 复用
  • Cache 缓存:视图滑出屏幕时暂存于此,用户反向滚动时可快速取回,无需重新绑定数据
  • RecycledViewPool:共享回收池,淘汰的视图会进入这里,可被多个 RecyclerView 实例复用,大幅减少视图创建开销

举个直观的例子:当你滑动一个 100 条数据的列表时,ListView 可能需要反复创建和销毁视图,而 RecyclerView 最多只创建 “屏幕可见数量 + 2” 个视图,后续完全通过复用实现,内存占用和渲染效率天差地别。

3. 功能扩展性:自带动画与交互支持

ListView 需要自定义实现的 item 增删动画、拖拽排序等功能,RecyclerView 通过ItemAnimatorItemTouchHelper提供了原生支持:

  • 内置默认动画:item 添加、删除、更新时的过渡动画
  • 自定义动画:通过重写ItemAnimator实现个性化效果
  • 拖拽排序 / 侧滑删除:借助ItemTouchHelper几行代码即可实现

4. 架构解耦:职责分明更易维护

RecyclerView 将数据展示、布局管理、交互逻辑拆分为独立模块:

  • Adapter:负责数据与视图的绑定
  • LayoutManager:负责视图的排列布局
  • ViewHolder:负责缓存 item 视图
  • ItemAnimator:负责 item 动画效果这种解耦设计让代码结构更清晰,后续维护和扩展更方便。

二、RecyclerView 基础入门:3 步实现一个标准列表

基础用法看似简单,但很多新手会在依赖配置、组件绑定等细节上踩坑。下面以 “展示用户列表” 为例,带你一步步实现一个可直接运行的 RecyclerView。

1. 第一步:配置依赖与布局文件

(1)引入 RecyclerView 依赖

RecyclerView 属于 AndroidX 库,需在 Module 级别的build.gradle中添加依赖(推荐使用最新稳定版):

dependencies {
    // RecyclerView核心依赖
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    // 可选:如果需要使用选择功能(如多选、单选)
    implementation 'androidx.recyclerview:recyclerview-selection:1.2.0'
}

同步项目后,即可在布局中使用 RecyclerView 组件。注意:避免使用旧版 support 库的 RecyclerView,AndroidX 已成为官方推荐标准。

(2)创建主布局(activity_main.xml)

在 Activity 布局中添加 RecyclerView,注意宽高设置为match_parent(避免显示异常):

<?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="match_parent"
    android:orientation="vertical">

    <!-- RecyclerView列表组件 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_user_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp" />

</LinearLayout>
(3)创建 Item 布局(item_user.xml)

每个列表项的布局,这里展示用户头像、姓名和简介:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="12dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp"
    app:contentPadding="16dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <!-- 用户头像 -->
        <ImageView
            android:id="@+id/iv_avatar"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher" />

        <!-- 文本信息容器 -->
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="16dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="18sp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/tv_desc"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:textSize="14sp"
                android:textColor="@android:color/darker_gray" />

        </LinearLayout>

    </LinearLayout>

</androidx.cardview.widget.CardView>

这里使用 CardView 包裹 item,让列表更具立体感,实际开发中可根据需求调整。

2. 第二步:创建数据模型与 ViewHolder

(1)数据模型(UserBean.java)

用于存储列表项的数据,根据 item 布局中的展示字段定义:

public class UserBean {
    // 用户名
    private String name;
    // 简介
    private String desc;
    // 头像资源ID(实际开发中可能是网络图片URL)
    private int avatarResId;

    // 构造方法
    public UserBean(String name, String desc, int avatarResId) {
        this.name = name;
        this.desc = desc;
        this.avatarResId = avatarResId;
    }

    // Getter和Setter方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public int getAvatarResId() {
        return avatarResId;
    }

    public void setAvatarResId(int avatarResId) {
        this.avatarResId = avatarResId;
    }
}
(2)创建 ViewHolder

ViewHolder 的核心作用是缓存 item 中的视图,避免反复调用findViewById(这是 RecyclerView 性能优化的关键之一):

public class UserViewHolder extends RecyclerView.ViewHolder {
    // 缓存item中的视图
    ImageView ivAvatar;
    TextView tvName;
    TextView tvDesc;

    // 构造方法:初始化视图
    public UserViewHolder(@NonNull View itemView) {
        super(itemView);
        // 绑定视图ID
        ivAvatar = itemView.findViewById(R.id.iv_avatar);
        tvName = itemView.findViewById(R.id.tv_name);
        tvDesc = itemView.findViewById(R.id.tv_desc);
    }

    // 绑定数据到视图
    public void bindData(UserBean userBean) {
        // 设置头像(实际开发中推荐用Glide/Picasso加载网络图片)
        ivAvatar.setImageResource(userBean.getAvatarResId());
        // 设置用户名
        tvName.setText(userBean.getName());
        // 设置简介
        tvDesc.setText(userBean.getDesc());
    }
}

将数据绑定逻辑封装在bindData方法中,让 Adapter 代码更简洁。

3. 第三步:实现 Adapter 与 Activity 绑定

Adapter 是 RecyclerView 的 “桥梁”,负责将数据传递给 ViewHolder,并创建 ViewHolder 实例。

(1)实现 Adapter(UserAdapter.java)
public class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
    // 数据集
    private List<UserBean> mUserList;

    // 构造方法:接收数据集
    public UserAdapter(List<UserBean> userList) {
        this.mUserList = userList;
    }

    // 第一步:创建ViewHolder(加载item布局)
    @NonNull
    @Override
    public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // 加载item布局文件
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_user, parent, false);
        // 返回ViewHolder实例
        return new UserViewHolder(itemView);
    }

    // 第二步:绑定数据到ViewHolder
    @Override
    public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
        // 获取当前位置的数据
        UserBean userBean = mUserList.get(position);
        // 调用ViewHolder的bindData方法绑定数据
        holder.bindData(userBean);
    }

    // 第三步:返回数据集大小
    @Override
    public int getItemCount() {
        // 避免空指针:数据集为空时返回0
        return mUserList == null ? 0 : mUserList.size();
    }

    // 可选:添加数据更新方法(后续刷新列表用)
    public void updateData(List<UserBean> newUserList) {
        this.mUserList = newUserList;
        // 通知RecyclerView数据已变更
        notifyDataSetChanged();
    }
}

Adapter 的核心是三个重写方法:onCreateViewHolder(创建视图)、onBindViewHolder(绑定数据)、getItemCount(返回数据量)。

(2)在 Activity 中初始化 RecyclerView
public class MainActivity extends AppCompatActivity {
    private RecyclerView mRvUserList;
    private UserAdapter mUserAdapter;
    private List<UserBean> mUserList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 初始化RecyclerView
        mRvUserList = findViewById(R.id.rv_user_list);

        // 2. 设置布局管理器(这里用线性布局,垂直方向)
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRvUserList.setLayoutManager(layoutManager);

        // 3. 初始化数据集
        initData();

        // 4. 初始化Adapter并设置给RecyclerView
        mUserAdapter = new UserAdapter(mUserList);
        mRvUserList.setAdapter(mUserAdapter);
    }

    // 初始化测试数据
    private void initData() {
        mUserList = new ArrayList<>();
        // 添加10条测试数据
        for (int i = 0; i < 10; i++) {
            mUserList.add(new UserBean(
                    "用户" + (i + 1),
                    "RecyclerView入门实战,第" + (i + 1) + "条数据",
                    R.mipmap.ic_launcher
            ));
        }
    }
}

到这里,一个标准的 RecyclerView 列表就实现了!运行项目,你会看到一个带卡片效果的垂直列表,滑动流畅且性能优异。


三、RecyclerView 进阶技巧:覆盖 80% 开发场景

基础用法只能满足简单列表需求,实际开发中会遇到各种复杂场景(如多类型布局、下拉刷新、侧滑删除等)。下面这些进阶技巧,让你应对大部分开发需求。

1. 多类型布局:一个列表展示不同样式 Item

很多场景需要在同一个列表中展示不同样式的 item,比如聊天列表(文字消息、图片消息、红包消息)、电商订单列表(待付款、待发货、已完成)。RecyclerView 通过getItemViewType方法轻松实现。

实战场景:聊天列表(文字消息 + 图片消息)
(1)定义消息类型常量与数据模型
// 消息类型常量
public static final int MESSAGE_TYPE_TEXT = 0; // 文字消息
public static final int MESSAGE_TYPE_IMAGE = 1; // 图片消息

// 消息数据模型
public class MessageBean {
    private int type; // 消息类型
    private String text; // 文字消息内容
    private String imageUrl; // 图片消息URL
    private boolean isSelf; // 是否是自己发送的消息

    // 构造方法、Getter、Setter省略...
}
(2)创建两种 ViewHolder
// 文字消息ViewHolder
public class TextMessageViewHolder extends RecyclerView.ViewHolder {
    TextView tvText;

    public TextMessageViewHolder(@NonNull View itemView) {
        super(itemView);
        tvText = itemView.findViewById(R.id.tv_text_message);
    }

    public void bindData(MessageBean messageBean) {
        tvText.setText(messageBean.getText());
        // 根据是否是自己发送的消息,调整文字对齐方式
        if (messageBean.isSelf()) {
            tvText.setGravity(Gravity.END);
            tvText.setBackgroundResource(R.drawable.bg_self_message);
        } else {
            tvText.setGravity(Gravity.START);
            tvText.setBackgroundResource(R.drawable.bg_other_message);
        }
    }
}

// 图片消息ViewHolder
public class ImageMessageViewHolder extends RecyclerView.ViewHolder {
    ImageView ivImage;

    public ImageMessageViewHolder(@NonNull View itemView) {
        super(itemView);
        ivImage = itemView.findViewById(R.id.iv_image_message);
    }

    public void bindData(MessageBean messageBean) {
        // 用Glide加载图片(需引入Glide依赖)
        Glide.with(itemView.getContext())
                .load(messageBean.getImageUrl())
                .placeholder(R.mipmap.ic_image_placeholder)
                .into(ivImage);
        // 调整图片对齐方式
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) ivImage.getLayoutParams();
        if (messageBean.isSelf()) {
            params.gravity = Gravity.END;
        } else {
            params.gravity = Gravity.START;
        }
        ivImage.setLayoutParams(params);
    }
}
(3)实现多类型 Adapter
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private List<MessageBean> mMessageList;

    public MessageAdapter(List<MessageBean> messageList) {
        this.mMessageList = messageList;
    }

    // 关键:返回当前item的类型
    @Override
    public int getItemViewType(int position) {
        return mMessageList.get(position).getType();
    }

    // 根据item类型创建对应的ViewHolder
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        if (viewType == MESSAGE_TYPE_TEXT) {
            // 加载文字消息布局
            View itemView = inflater.inflate(R.layout.item_text_message, parent, false);
            return new TextMessageViewHolder(itemView);
        } else {
            // 加载图片消息布局
            View itemView = inflater.inflate(R.layout.item_image_message, parent, false);
            return new ImageMessageViewHolder(itemView);
        }
    }

    // 根据ViewHolder类型绑定对应数据
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        MessageBean messageBean = mMessageList.get(position);
        if (holder instanceof TextMessageViewHolder) {
            ((TextMessageViewHolder) holder).bindData(messageBean);
        } else if (holder instanceof ImageMessageViewHolder) {
            ((ImageMessageViewHolder) holder).bindData(messageBean);
        }
    }

    @Override
    public int getItemCount() {
        return mMessageList == null ? 0 : mMessageList.size();
    }
}

核心逻辑:通过getItemViewType返回 item 类型,在onCreateViewHolder中根据类型加载不同布局,onBindViewHolder中根据 ViewHolder 类型绑定数据。

2. 下拉刷新与上拉加载:实现无限滚动列表

下拉刷新(PullToRefresh)和上拉加载更多(LoadMore)是列表的常用功能,RecyclerView 结合 SwipeRefreshLayout 和滚动监听即可实现。

(1)布局文件中添加 SwipeRefreshLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_news_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
(2)在 Activity 中实现下拉刷新逻辑
public class NewsActivity extends AppCompatActivity {
    private SwipeRefreshLayout mSwipeRefreshLayout;
    private RecyclerView mRvNewsList;
    private NewsAdapter mNewsAdapter;
    private List<NewsBean> mNewsList = new ArrayList<>();
    private int mPage = 1; // 当前页码
    private static final int PAGE_SIZE = 10; // 每页数据量

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news);

        // 初始化控件
        mSwipeRefreshLayout = findViewById(R.id.swipe_refresh_layout);
        mRvNewsList = findViewById(R.id.rv_news_list);

        // 设置RecyclerView布局管理器
        mRvNewsList.setLayoutManager(new LinearLayoutManager(this));

        // 初始化Adapter
        mNewsAdapter = new NewsAdapter(mNewsList);
        mRvNewsList.setAdapter(mNewsAdapter);

        // 设置下拉刷新样式
        mSwipeRefreshLayout.setColorSchemeResources(
                android.R.color.holo_blue_light,
                android.R.color.holo_green_light,
                android.R.color.holo_orange_light
        );

        // 下拉刷新监听
        mSwipeRefreshLayout.setOnRefreshListener(() -> {
            // 下拉刷新时重置页码
            mPage = 1;
            // 模拟网络请求加载数据
            loadNewsData(true);
        });

        // 上拉加载更多:监听RecyclerView滚动事件
        mRvNewsList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                // 获取布局管理器
                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                if (layoutManager == null) return;

                // 已加载的item总数
                int totalItemCount = layoutManager.getItemCount();
                // 最后一个可见item的位置
                int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();

                // 条件:滑动到底部 + 不是正在加载中 + 还有更多数据
                if (lastVisibleItemPosition >= totalItemCount - 1 
                        && !mIsLoading 
                        && mHasMoreData) {
                    // 加载下一页数据
                    loadNewsData(false);
                }
            }
        });

        // 首次加载数据
        loadNewsData(true);
    }

    // 是否正在加载中
    private boolean mIsLoading = false;
    // 是否还有更多数据
    private boolean mHasMoreData = true;

    // 模拟网络请求加载新闻数据
    private void loadNewsData(boolean isRefresh) {
        mIsLoading = true;
        // 如果是下拉刷新,显示刷新动画
        if (isRefresh) {
            mSwipeRefreshLayout.setRefreshing(true);
        } else {
            // 上拉加载时,在列表底部添加加载提示
            mNewsAdapter.addLoadMoreFooter();
        }

        // 模拟网络请求(实际开发中替换为Retrofit/OkHttp请求)
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            List<NewsBean> newData = new ArrayList<>();
            // 模拟生成10条测试数据
            for (int i = 0; i < PAGE_SIZE; i++) {
                int index = (mPage - 1) * PAGE_SIZE + i;
                newData.add(new NewsBean(
                        "新闻标题" + index,
                        "新闻摘要:RecyclerView下拉刷新与上拉加载实战案例...",
                        "2025-11-13",
                        "https://example.com/image" + index + ".jpg"
                ));
            }

            // 处理数据
            if (isRefresh) {
                // 下拉刷新:清空原有数据,添加新数据
                mNewsList.clear();
                mNewsList.addAll(newData);
                // 重置"是否有更多数据"状态
                mHasMoreData = true;
            } else {
                // 上拉加载:添加新数据
                mNewsList.addAll(newData);
                // 模拟加载5页后没有更多数据
                if (mPage >= 5) {
                    mHasMoreData = false;
                }
            }

            // 更新Adapter数据
            mNewsAdapter.notifyDataSetChanged();

            // 加载完成后更新状态
            mIsLoading = false;
            if (isRefresh) {
                mSwipeRefreshLayout.setRefreshing(false);
            } else {
                // 移除加载提示
                mNewsAdapter.removeLoadMoreFooter();
                // 如果没有更多数据,添加"没有更多"提示
                if (!mHasMoreData) {
                    mNewsAdapter.addNoMoreFooter();
                }
            }

            // 页码+1
            mPage++;
        }, 1500); // 模拟网络延迟1.5秒
    }
}
(3)Adapter 中添加 Footer 处理
public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int ITEM_TYPE_CONTENT = 0; // 内容item
    private static final int ITEM_TYPE_LOAD_MORE = 1; // 加载中Footer
    private static final int ITEM_TYPE_NO_MORE = 2; // 没有更多Footer

    private List<NewsBean> mNewsList;
    private boolean mShowLoadMoreFooter = false;
    private boolean mShowNoMoreFooter = false;

    // 构造方法省略...

    // 重写getItemViewType,添加Footer类型判断
    @Override
    public int getItemViewType(int position) {
        if (mShowNoMoreFooter && position == getItemCount() - 1) {
            return ITEM_TYPE_NO_MORE;
        }
        if (mShowLoadMoreFooter && position == getItemCount() - 1) {
            return ITEM_TYPE_LOAD_MORE;
        }
        return ITEM_TYPE_CONTENT;
    }

    // 重写getItemCount,添加Footer数量
    @Override
    public int getItemCount() {
        int baseCount = mNewsList == null ? 0 : mNewsList.size();
        if (mShowLoadMoreFooter || mShowNoMoreFooter) {
            baseCount += 1; // 增加一个Footer
        }
        return baseCount;
    }

    // 根据类型创建ViewHolder
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        if (viewType == ITEM_TYPE_CONTENT) {
            View itemView = inflater.inflate(R.layout.item_news, parent, false);
            return new NewsViewHolder(itemView);
        } else if (viewType == ITEM_TYPE_LOAD_MORE) {
            View itemView = inflater.inflate(R.layout.item_load_more, parent, false);
            return new LoadMoreViewHolder(itemView);
        } else {
            View itemView = inflater.inflate(R.layout.item_no_more, parent, false);
            return new NoMoreViewHolder(itemView);
        }
    }

    // 绑定数据(Footer不需要绑定数据)
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof NewsViewHolder) {
            // 注意:Footer会占用position,内容item的position需要调整
            int contentPosition = position;
            NewsBean newsBean = mNewsList.get(contentPosition);
            ((NewsViewHolder) holder).bindData(newsBean);
        }
    }

    // 添加加载中Footer
    public void addLoadMoreFooter() {
        mShowLoadMoreFooter = true;
        mShowNoMoreFooter = false;
        notifyItemInserted(getItemCount() - 1);
    }

    // 移除加载中Footer
    public void removeLoadMoreFooter() {
        if (mShowLoadMoreFooter) {
            mShowLoadMoreFooter = false;
            notifyItemRemoved(getItemCount());
        }
    }

    // 添加没有更多Footer
    public void addNoMoreFooter() {
        mShowNoMoreFooter = true;
        mShowLoadMoreFooter = false;
        notifyItemInserted(getItemCount() - 1);
    }

    // 内容ViewHolder(新闻item)
    public static class NewsViewHolder extends RecyclerView.ViewHolder {
        // 实现省略,类似之前的UserViewHolder
    }

    // 加载中Footer的ViewHolder
    public static class LoadMoreViewHolder extends RecyclerView.ViewHolder {
        public LoadMoreViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    // 没有更多Footer的ViewHolder
    public static class NoMoreViewHolder extends RecyclerView.ViewHolder {
        public NoMoreViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

注意事项:上拉加载时要处理 “正在加载中” 状态,避免重复请求;Footer 会占用 item 位置,绑定数据时要注意 position 校准。

3. Item 动画:让列表交互更流畅

RecyclerView 内置了默认的 item 动画,但也支持自定义动画效果,让列表增删改查的交互更生动。

(1)使用默认动画

只需在初始化 RecyclerView 时设置ItemAnimator

// 设置默认动画(API 21+支持)
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
// 可选:设置动画持续时间
((DefaultItemAnimator) mRecyclerView.getItemAnimator()).setAddDuration(300);
((DefaultItemAnimator) mRecyclerView.getItemAnimator()).setRemoveDuration(300);

默认动画支持 item 添加、删除、更新时的过渡效果,满足基础需求。

(2)自定义 Item 动画(示例:淡入淡出效果)
public class FadeInAnimator extends SimpleItemAnimator {
    private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
    private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();

    // 处理item添加动画
    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        mPendingAdditions.add(holder);
        // 初始状态:透明、缩放0.8
        holder.itemView.setAlpha(0f);
        holder.itemView.setScaleX(0.8f);
        holder.itemView.setScaleY(0.8f);
        // 执行动画:300毫秒内恢复正常状态
        holder.itemView.animate()
                .alpha(1f)
                .scaleX(1f)
                .scaleY(1f)
                .setDuration(300)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // 动画结束后通知
                        dispatchAddFinished(holder);
                        mPendingAdditions.remove(holder);
                    }
                })
                .start();
        return true;
    }

    // 处理item删除动画
    @Override
    public boolean animateRemove(RecyclerView.ViewHolder holder) {
        mPendingRemovals.add(holder);
        // 执行动画:300毫秒内透明消失
        holder.itemView.animate()
                .alpha(0f)
                .setDuration(300)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        dispatchRemoveFinished(holder);
                        mPendingRemovals.remove(holder);
                    }
                })
                .start();
        return true;
    }

    // 其他抽象方法实现(省略,可参考官方文档)
    @Override
    public void runPendingAnimations() {}

    @Override
    public void endAnimation(RecyclerView.ViewHolder holder) {}

    @Override
    public void endAnimations() {}

    @Override
    public boolean isRunning() {
        return !mPendingRemovals.isEmpty() || !mPendingAdditions.isEmpty();
    }
}

使用自定义动画:

mRecyclerView.setItemAnimator(new FadeInAnimator());

4. 点击事件与长按事件:优雅实现交互

RecyclerView 没有像 ListView 那样直接提供setOnItemClickListener方法,需要我们自己通过接口回调实现。

(1)在 Adapter 中定义点击事件接口
public class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
    // 定义点击事件接口
    public interface OnItemClickListener {
        void onItemClick(int position, UserBean userBean);
    }

    // 定义长按事件接口
    public interface OnItemLongClickListener {
        boolean onItemLongClick(int position, UserBean userBean);
    }

    // 声明接口变量
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;

    // 设置点击事件监听
    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mOnItemClickListener = listener;
    }

    // 设置长按事件监听
    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
        this.mOnItemLongClickListener = listener;
    }

    // 在onBindViewHolder中设置点击事件
    @Override
    public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
        UserBean userBean = mUserList.get(position);
        holder.bindData(userBean);

        // 点击事件
        holder.itemView.setOnClickListener(v -> {
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemClick(position, userBean);
            }
        });

        // 长按事件
        holder.itemView.setOnLongClickListener(v -> {
            if (mOnItemLongClickListener != null) {
                return mOnItemLongClickListener.onItemLongClick(position, userBean);
            }
            return false;
        });
    }

    // 其他方法省略...
}
(2)在 Activity 中设置监听
mUserAdapter.setOnItemClickListener((position, userBean) -> {
    // 处理点击事件,比如跳转到用户详情页
    Toast.makeText(MainActivity.this, "点击了" + userBean.getName(), Toast.LENGTH_SHORT).show();
    Intent intent = new Intent(MainActivity.this, UserDetailActivity.class);
    intent.putExtra("USER_DATA", userBean);
    startActivity(intent);
});

mUserAdapter.setOnItemLongClickListener((position, userBean) -> {
    // 处理长按事件,比如弹出删除菜单
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("提示")
            .setMessage("是否删除" + userBean.getName() + "?")
            .setPositiveButton("删除", (dialog, which) -> {
                // 从数据集移除数据
                mUserList.remove(position);
                // 通知RecyclerView删除对应item
                mUserAdapter.notifyItemRemoved(position);
                // 注意:删除后需要刷新后续item的position,避免错位
                mUserAdapter.notifyItemRangeChanged(position, mUserList.size() - position);
            })
            .setNegativeButton("取消", null)
            .show();
    return true; // 消费长按事件,避免触发点击事件
});

注意:删除 item 后,需要调用notifyItemRangeChanged刷新后续 item 的 position,否则会导致数据错位。


四、RecyclerView 性能优化:从流畅到极致

RecyclerView 本身性能优异,但如果使用不当,仍会出现卡顿、掉帧等问题。下面这些优化技巧,能让你的列表滑动更丝滑。

1. 优化数据更新:用 DiffUtil 替代 notifyDataSetChanged

notifyDataSetChanged是 “暴力刷新”,会导致所有可见 item 重新绑定数据,性能损耗大。DiffUtil 能精确计算新旧数据的差异,只更新变化的 item。

(1)实现 DiffUtil.Callback
public class UserDiffCallback extends DiffUtil.Callback {
    private List<UserBean> mOldList;
    private List<UserBean> mNewList;

    public UserDiffCallback(List<UserBean> oldList, List<UserBean> newList) {
        this.mOldList = oldList;
        this.mNewList = newList;
    }

    // 旧数据和新数据的大小对比
    @Override
    public int getOldListSize() {
        return mOldList == null ? 0 : mOldList.size();
    }

    @Override
    public int getNewListSize() {
        return mNewList == null ? 0 : mNewList.size();
    }

    // 判断两个item是否是同一个对象(通常用唯一ID判断)
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        UserBean oldUser = mOldList.get(oldItemPosition);
        UserBean newUser = mNewList.get(newItemPosition);
        // 假设UserBean有唯一ID字段userId
        return oldUser.getUserId().equals(newUser.getUserId());
    }

    // 判断两个item的内容是否相同
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        UserBean oldUser = mOldList.get(oldItemPosition);
        UserBean newUser = mNewList.get(newItemPosition);
        // 对比所有字段是否相同
        return oldUser.getName().equals(newUser.getName())
                && oldUser.getDesc().equals(newUser.getDesc())
                && oldUser.getAvatarResId() == newUser.getAvatarResId();
    }

    // 可选:如果内容不同,返回差异字段(用于局部刷新)
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        UserBean oldUser = mOldList.get(oldItemPosition);
        UserBean newUser = mNewList.get(newItemPosition);

        Bundle payload = new Bundle();
        if (!oldUser.getName().equals(newUser.getName())) {
            payload.putString("NAME", newUser.getName());
        }
        if (!oldUser.getDesc().equals(newUser.getDesc())) {
            payload.putString("DESC", newUser.getDesc());
        }

        return payload.isEmpty() ? null : payload;
    }
}
(2)在 Adapter 中使用 DiffUtil
public class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
    // 旧数据集
    private List<UserBean> mOldUserList = new ArrayList<>();

    // 用DiffUtil更新数据
    public void updateData(List<UserBean> newUserList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
                new UserDiffCallback(mOldUserList, newUserList),
                true // 是否检测item内容的移动
        );

        // 更新数据集
        mOldUserList.clear();
        mOldUserList.addAll(newUserList);

        // 应用差异更新
        diffResult.dispatchUpdatesTo(this);
    }

    // 重写onBindViewHolder,支持局部刷新
    @Override
    public void onBindViewHolder(@NonNull UserViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            // 没有差异,正常绑定全部数据
            super.onBindViewHolder(holder, position, payloads);
        } else {
            // 有差异,只更新变化的字段
            Bundle payload = (Bundle) payloads.get(0);
            UserBean userBean = mOldUserList.get(position);
            if (payload.containsKey("NAME")) {
                holder.tvName.setText(payload.getString("NAME"));
            }
            if (payload.containsKey("DESC")) {
                holder.tvDesc.setText(payload.getString("DESC"));
            }
        }
    }

    // 其他方法省略...
}

使用 DiffUtil 后,即使列表有 100 条数据,只变化 1 条,也只会更新这 1 条 item,性能提升明显。

2. 布局优化:减少层级与过度绘制

(1)简化 item 布局

item 布局层级越多,渲染时间越长。建议:

  • 使用 ConstraintLayout 替代 LinearLayout 嵌套,减少布局层级
  • 避免不必要的 View(如空的 TextView、ImageView)
  • 合理使用visibility="gone"替代移除 View
(2)设置 RecyclerView 的 ItemViewCacheSize

默认情况下,RecyclerView 的缓存数量有限,可通过setItemViewCacheSize增加缓存数量,减少onCreateViewHolder的调用:

mRecyclerView.setItemViewCacheSize(20); // 缓存20个item视图
(3)关闭过度绘制

在开发者选项中开启 “显示过度绘制”,避免 item 布局中存在多层背景叠加(如 item 背景 + CardView 背景 + 布局背景)。

3. 图片加载优化:避免 OOM 与卡顿

列表中图片加载是性能瓶颈之一,需注意:

  • 使用 Glide/Picasso 等图片加载库,支持图片压缩、缓存、异步加载
  • 图片尺寸与 ImageView 大小匹配,避免大图缩放
  • 滑动时暂停图片加载,滑动停止后恢复:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            // 滑动停止,恢复图片加载
            Glide.with(MainActivity.this).resumeRequests();
        } else {
            // 滑动中,暂停图片加载
            Glide.with(MainActivity.this).pauseRequests();
        }
    }
});

4. 避免在 onBindViewHolder 中做耗时操作

onBindViewHolder在主线程执行,耗时操作会导致卡顿:

  • 避免在其中创建对象(如 String、List),应提前初始化或复用
  • 避免复杂计算,可在子线程预处理数据
  • 避免频繁调用notifyItemChanged,尽量批量更新

五、实战案例:打造电商 App 商品列表(支持布局切换)

结合前面的知识点,实现一个仿京东商品列表,支持 “线性布局” 与 “网格布局” 切换,包含下拉刷新、上拉加载、商品点击跳转等功能。

1. 功能需求

  • 两种布局切换:线性布局(单列,显示商品详情)、网格布局(双列,简洁展示)
  • 下拉刷新:更新商品列表
  • 上拉加载:加载更多商品
  • 商品点击:跳转到商品详情页
  • 支持商品收藏、加入购物车功能

2. 核心代码实现

(1)布局文件(activity_goods_list.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="match_parent"
    android:orientation="vertical">

    <!-- 顶部工具栏:包含标题和布局切换按钮 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:background="@android:color/white"
        android:gravity="center_vertical"
        android:paddingHorizontal="16dp">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="商品列表"
            android:textSize="20sp"
            android:textStyle="bold" />

        <!-- 布局切换按钮 -->
        <ImageView
            android:id="@+id/iv_layout_switch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_grid_layout"
            android:contentDescription="切换布局" />

    </LinearLayout>

    <!-- 下拉刷新控件 -->
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh_goods"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- 商品列表RecyclerView -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_goods_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>
(2)两种 Item 布局
  • 线性布局 item(item_goods_linear.xml):单列,显示商品图片、名称、价格、销量、好评率
  • 网格布局 item(item_goods_grid.xml):双列,只显示商品图片、名称、价格
(3)Adapter 实现(GoodsAdapter.java)
public class GoodsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int LAYOUT_TYPE_LINEAR = 0; // 线性布局
    private static final int LAYOUT_TYPE_GRID = 1; // 网格布局

    private List<GoodsBean> mGoodsList;
    private int mCurrentLayoutType = LAYOUT_TYPE_LINEAR; // 当前布局类型

    // 点击事件接口
    public interface OnGoodsClickListener {
        void onGoodsClick(GoodsBean goodsBean);
        void onCollectClick(int position, GoodsBean goodsBean);
        void onAddCartClick(int position, GoodsBean goodsBean);
    }

    private OnGoodsClickListener mOnGoodsClickListener;

    public void setOnGoodsClickListener(OnGoodsClickListener listener) {
        this.mOnGoodsClickListener = listener;
    }

    // 构造方法
    public GoodsAdapter(List<GoodsBean> goodsList) {
        this.mGoodsList = goodsList;
    }

    // 设置布局类型并刷新
    public void setLayoutType(int layoutType) {
        mCurrentLayoutType = layoutType;
        notifyDataSetChanged(); // 布局切换需刷新全部item
    }

    // 获取当前布局类型
    public int getCurrentLayoutType() {
        return mCurrentLayoutType;
    }

    // 根据布局类型返回item视图类型
    @Override
    public int getItemViewType(int position) {
        return mCurrentLayoutType;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        if (viewType == LAYOUT_TYPE_LINEAR) {
            // 加载线性布局item
            View itemView = inflater.inflate(R.layout.item_goods_linear, parent, false);
            return new LinearGoodsViewHolder(itemView);
        } else {
            // 加载网格布局item
            View itemView = inflater.inflate(R.layout.item_goods_grid, parent, false);
            return new GridGoodsViewHolder(itemView);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        GoodsBean goodsBean = mGoodsList.get(position);
        if (holder instanceof LinearGoodsViewHolder) {
            ((LinearGoodsViewHolder) holder).bindData(goodsBean, position);
        } else if (holder instanceof GridGoodsViewHolder) {
            ((GridGoodsViewHolder) holder).bindData(goodsBean, position);
        }
    }

    @Override
    public int getItemCount() {
        return mGoodsList == null ? 0 : mGoodsList.size();
    }

    // 更新数据(使用DiffUtil)
    public void updateData(List<GoodsBean> newGoodsList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
                new GoodsDiffCallback(mGoodsList, newGoodsList), true);
        mGoodsList.clear();
        mGoodsList.addAll(newGoodsList);
        diffResult.dispatchUpdatesTo(this);
    }

    // 线性布局ViewHolder
    public class LinearGoodsViewHolder extends RecyclerView.ViewHolder {
        ImageView ivGoodsImage;
        TextView tvGoodsName;
        TextView tvPrice;
        TextView tvSales;
        TextView tvRating;
        ImageView ivCollect;
        Button btnAddCart;

        public LinearGoodsViewHolder(@NonNull View itemView) {
            super(itemView);
            // 绑定视图(省略)
        }

        public void bindData(GoodsBean goodsBean, int position) {
            // 加载商品图片
            Glide.with(itemView.getContext())
                    .load(goodsBean.getImageUrl())
                    .placeholder(R.mipmap.ic_goods_placeholder)
                    .into(ivGoodsImage);

            // 设置商品名称、价格、销量、好评率(省略)

            // 收藏按钮状态
            ivCollect.setImageResource(goodsBean.isCollected() 
                    ? R.mipmap.ic_collected : R.mipmap.ic_uncollect);

            // 商品点击事件
            itemView.setOnClickListener(v -> {
                if (mOnGoodsClickListener != null) {
                    mOnGoodsClickListener.onGoodsClick(goodsBean);
                }
            });

            // 收藏点击事件
            ivCollect.setOnClickListener(v -> {
                if (mOnGoodsClickListener != null) {
                    mOnGoodsClickListener.onCollectClick(position, goodsBean);
                }
            });

            // 加入购物车点击事件
            btnAddCart.setOnClickListener(v -> {
                if (mOnGoodsClickListener != null) {
                    mOnGoodsClickListener.onAddCartClick(position, goodsBean);
                }
            });
        }
    }

    // 网格布局ViewHolder(实现类似,省略)
    public class GridGoodsViewHolder extends RecyclerView.ViewHolder {
        // 实现省略...
    }

    // 商品DiffCallback(实现类似之前的UserDiffCallback,省略)
    private static class GoodsDiffCallback extends DiffUtil.Callback {
        // 实现省略...
    }
}
(4)Activity 实现(GoodsListActivity.java)
public class GoodsListActivity extends AppCompatActivity {
    private SwipeRefreshLayout mSwipeRefreshLayout;
    private RecyclerView mRvGoodsList;
    private GoodsAdapter mGoodsAdapter;
    private ImageView mIvLayoutSwitch;
    private List<GoodsBean> mGoodsList = new ArrayList<>();

    private int mPage = 1;
    private boolean mIsLoading = false;
    private boolean mHasMoreData = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_goods_list);

        // 初始化控件
        initView();

        // 初始化数据
        loadGoodsData(true);
    }

    private void initView() {
        mSwipeRefreshLayout = findViewById(R.id.swipe_refresh_goods);
        mRvGoodsList = findViewById(R.id.rv_goods_list);
        mIvLayoutSwitch = findViewById(R.id.iv_layout_switch);

        // 初始化RecyclerView(默认线性布局)
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        mRvGoodsList.setLayoutManager(linearLayoutManager);

        // 初始化Adapter
        mGoodsAdapter = new GoodsAdapter(mGoodsList);
        mRvGoodsList.setAdapter(mGoodsAdapter);

        // 布局切换点击事件
        mIvLayoutSwitch.setOnClickListener(v -> {
            int currentType = mGoodsAdapter.getCurrentLayoutType();
            if (currentType == GoodsAdapter.LAYOUT_TYPE_LINEAR) {
                // 切换到网格布局(2列)
                mGoodsAdapter.setLayoutType(GoodsAdapter.LAYOUT_TYPE_GRID);
                mRvGoodsList.setLayoutManager(new GridLayoutManager(this, 2));
                mIvLayoutSwitch.setImageResource(R.mipmap.ic_linear_layout);
            } else {
                // 切换到线性布局
                mGoodsAdapter.setLayoutType(GoodsAdapter.LAYOUT_TYPE_LINEAR);
                mRvGoodsList.setLayoutManager(new LinearLayoutManager(this));
                mIvLayoutSwitch.setImageResource(R.mipmap.ic_grid_layout);
            }
        });

        // 下拉刷新监听
        mSwipeRefreshLayout.setOnRefreshListener(() -> {
            mPage = 1;
            loadGoodsData(true);
        });

        // 上拉加载监听
        mRvGoodsList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
                    int totalItemCount = linearManager.getItemCount();
                    int lastVisibleItem = linearManager.findLastVisibleItemPosition();
                    if (!mIsLoading && mHasMoreData && lastVisibleItem >= totalItemCount - 1) {
                        loadGoodsData(false);
                    }
                } else if (layoutManager instanceof GridLayoutManager) {
                    GridLayoutManager gridManager = (GridLayoutManager) layoutManager;
                    int totalItemCount = gridManager.getItemCount();
                    int lastVisibleItem = gridManager.findLastVisibleItemPosition();
                    if (!mIsLoading && mHasMoreData && lastVisibleItem >= totalItemCount - 2) {
                        loadGoodsData(false);
                    }
                }
            }
        });

        // 商品点击事件
        mGoodsAdapter.setOnGoodsClickListener(new GoodsAdapter.OnGoodsClickListener() {
            @Override
            public void onGoodsClick(GoodsBean goodsBean) {
                // 跳转到商品详情页
                Intent intent = new Intent(GoodsListActivity.this, GoodsDetailActivity.class);
                intent.putExtra("GOODS_DATA", goodsBean);
                startActivity(intent);
            }

            @Override
            public void onCollectClick(int position, GoodsBean goodsBean) {
                // 切换收藏状态
                goodsBean.setCollected(!goodsBean.isCollected());
                mGoodsAdapter.notifyItemChanged(position);
                // 实际开发中需同步到服务器(省略)
            }

            @Override
            public void onAddCartClick(int position, GoodsBean goodsBean) {
                // 加入购物车逻辑(省略)
                Toast.makeText(GoodsListActivity.this, "已加入购物车", Toast.LENGTH_SHORT).show();
            }
        });
    }

    // 加载商品数据(模拟网络请求)
    private void loadGoodsData(boolean isRefresh) {
        // 实现类似新闻列表的下拉刷新和上拉加载逻辑(省略)
    }
}

3. 效果演示

  • 初始显示线性布局,展示商品完整信息
  • 点击顶部切换按钮,切换为双列网格布局,简洁展示商品
  • 下拉刷新可更新商品列表,上拉加载更多商品
  • 点击商品跳转到详情页,点击收藏按钮切换收藏状态,点击加入购物车按钮弹出提示

六、实战案例:订单列表(展开 / 收起 + 嵌套 RecyclerView)

另一常见场景:订单列表,每个订单可展开 / 收起,展开后显示订单中的商品列表(嵌套 RecyclerView)。

1. 核心思路

  • 外层 RecyclerView:展示订单信息(订单号、时间、金额、展开 / 收起按钮)
  • 内层 RecyclerView:每个订单展开后,展示该订单下的商品列表
  • 通过订单数据模型中的isExpanded字段控制展开 / 收起状态
  • 点击展开 / 收起按钮时,更新isExpanded状态并刷新对应 item

2. 关键代码

(1)订单数据模型(OrderBean.java)
public class OrderBean {
    private String orderId; // 订单号
    private String orderTime; // 下单时间
    private double totalPrice; // 订单总金额
    private boolean isExpanded; // 是否展开
    private List<OrderGoodsBean> goodsList; // 订单中的商品列表

    // Getter、Setter、构造方法省略...
}
(2)外层 Adapter(OrderAdapter.java)
public class OrderAdapter extends RecyclerView.Adapter<OrderAdapter.OrderViewHolder> {
    private List<OrderBean> mOrderList;

    public OrderAdapter(List<OrderBean> orderList) {
        this.mOrderList = orderList;
    }

    @NonNull
    @Override
    public OrderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_order, parent, false);
        return new OrderViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull OrderViewHolder holder, int position) {
        OrderBean orderBean = mOrderList.get(position);
        holder.bindData(orderBean, position);
    }

    @Override
    public int getItemCount() {
        return mOrderList == null ? 0 : mOrderList.size();
    }

    public class OrderViewHolder extends RecyclerView.ViewHolder {
        TextView tvOrderId;
        TextView tvOrderTime;
        TextView tvTotalPrice;
        TextView tvExpand;
        RecyclerView rvOrderGoods;
        OrderGoodsAdapter mGoodsAdapter;

        public OrderViewHolder(@NonNull View itemView) {
            super(itemView);
            // 绑定外层订单视图(省略)
            rvOrderGoods = itemView.findViewById(R.id.rv_order_goods);
            // 初始化内层RecyclerView(网格布局,2列)
            rvOrderGoods.setLayoutManager(new GridLayoutManager(itemView.getContext(), 2));
            mGoodsAdapter = new OrderGoodsAdapter(new ArrayList<>());
            rvOrderGoods.setAdapter(mGoodsAdapter);
        }

        public void bindData(OrderBean orderBean, int position) {
            // 设置订单号、时间、总金额(省略)

            // 控制商品列表显示/隐藏
            if (orderBean.isExpanded()) {
                rvOrderGoods.setVisibility(View.VISIBLE);
                tvExpand.setText("收起");
                // 更新内层商品列表数据
                mGoodsAdapter.updateData(orderBean.getGoodsList());
            } else {
                rvOrderGoods.setVisibility(View.GONE);
                tvExpand.setText("展开");
            }

            // 展开/收起点击事件
            tvExpand.setOnClickListener(v -> {
                // 切换展开状态
                orderBean.setExpanded(!orderBean.isExpanded());
                // 刷新当前item
                notifyItemChanged(position);
            });
        }
    }
}
(3)内层 Adapter(OrderGoodsAdapter.java)

用于展示订单中的商品列表,实现类似商品列表的 ViewHolder 和数据绑定(省略,参考前面的 GoodsAdapter)。

3. 注意事项

  • 嵌套 RecyclerView 可能导致滑动冲突,需设置内层 RecyclerView 的嵌套滚动:
// 允许内层RecyclerView嵌套滚动
ViewCompat.setNestedScrollingEnabled(rvOrderGoods, true);
  • 展开 / 收起时使用notifyItemChanged刷新 item,避免全局刷新
  • 内层 RecyclerView 的布局管理器需根据需求选择(线性 / 网格)

七、RecyclerView 常见问题与避坑指南

1. 数据错位问题

原因:
  • ViewHolder 复用导致未重置视图状态(如收藏按钮状态、选中状态)
  • 删除 item 后未刷新后续 item 的 position
解决方案:
  • onBindViewHolder中重置所有视图状态(不要依赖默认状态)
  • 删除 item 后调用notifyItemRangeChanged刷新后续 item
  • 使用 DiffUtil 精确更新数据,避免批量刷新

2. 滑动卡顿问题

原因:
  • item 布局层级过深
  • onBindViewHolder中存在耗时操作
  • 图片加载未优化
  • 未使用 DiffUtil,频繁调用notifyDataSetChanged
解决方案:
  • 简化 item 布局,使用 ConstraintLayout 减少层级
  • 避免在onBindViewHolder中创建对象、复杂计算
  • 图片加载使用 Glide/Picasso,滑动时暂停加载
  • 用 DiffUtil 替代notifyDataSetChanged

3. 布局显示异常(item 不显示 / 显示不全)

原因:
  • RecyclerView 或 item 的宽高设置错误(如设置为wrap_content但未正确测量)
  • 未设置 LayoutManager(RecyclerView 必须设置 LayoutManager 才能显示)
  • 数据集为空或 Adapter 未设置给 RecyclerView
解决方案:
  • 确保 RecyclerView 的宽高为match_parent,item 布局根据需求设置
  • 初始化 RecyclerView 后必须调用setLayoutManager
  • 检查数据集是否为空,Adapter 是否正确设置

4. 点击事件失效

原因:
  • item 布局中存在可点击控件(如 Button、ImageView),抢占了 item 的点击事件
  • 点击事件监听未正确设置(如忘记调用setOnItemClickListener
解决方案:
  • 给可点击控件设置android:clickable="false",或在代码中设置setClickable(false)
  • 确保点击事件接口已正确实现并设置给 Adapter

八、总结与扩展

RecyclerView 的核心优势在于 “灵活” 和 “高效”,通过 LayoutManager、Adapter、ViewHolder 的解耦设计,让它能适配几乎所有列表场景。本文从基础用法到进阶技巧,再到两个实战案例,覆盖了 90% 的开发需求,只要掌握这些内容,就能在实际开发中灵活运用 RecyclerView。

扩展学习方向

  • 自定义 LayoutManager:实现特殊的列表布局(如环形列表、瀑布流增强版)
  • 结合 Jetpack 组件:使用 Paging 3 实现分页加载,Room 实现本地数据缓存
  • 复杂交互:item 拖拽排序、侧滑删除(使用 ItemTouchHelper)
  • 性能监控:通过 RecyclerView 的RecyclerView.ItemAnimatorRecyclerView.OnScrollListener监控滑动性能

如果你在使用 RecyclerView 时遇到了特定问题,或者需要实现某个复杂场景,可以在评论区留言,我会为你提供针对性的解决方案!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值