You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为RecyclerView实现无限滚动加载更多功能?

实现RecyclerView无限滚动的完整方案

我来帮你基于现有代码实现RecyclerView的无限滚动效果,同时保证加载效率和应用流畅度,咱们一步步来修改:


1. 核心修改:添加滚动监听与分页逻辑

首先要给RecyclerView添加滚动监听,判断用户滚动到列表底部时触发加载更多,同时要避免重复请求(比如用户快速滚动时多次触发)。

需要在MainActivity中新增几个变量:

private int currentPage = 1; // 当前页码,从1开始
private int pageSize = 10; // 每页加载的数据量,根据你的接口调整
private boolean isLoading = false; // 标记是否正在加载,防止重复请求

然后在onCreate方法里添加滚动监听:

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        
        // 只处理向下滚动的情况
        if (dy <= 0) return;
        
        int visibleItemCount = mLayoutManager.getChildCount();
        int totalItemCount = mLayoutManager.getItemCount();
        int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
        
        // 判断是否滚动到了列表底部,且没有在加载中
        if (!isLoading && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount) {
            currentPage++;
            getRetrofit(currentPage); // 加载下一页数据
            isLoading = true; // 标记为正在加载
            // 可以在这里给Adapter添加一个加载中的占位项
            list.add(new Model(Model.LOADING_TYPE, "", "", "", "", ""));
            adapter.notifyItemInserted(list.size() - 1);
        }
    }
});

2. 修改Retrofit请求支持分页

你的接口需要支持分页参数(大部分后端接口会用page或者offset参数),所以要修改RetrofitArrayApi的方法,添加页码参数:

public interface RetrofitArrayApi {
    @GET("your-post-endpoint") // 替换成你的实际接口路径
    Call<List<WPPost>> getPostInfo(@Query("page") int page, @Query("per_page") int perPage);
}

然后修改MainActivity里的getRetrofit方法,接收页码参数:

private void getRetrofit(int page) {
    Retrofit retrofit=new Retrofit.Builder()
           .baseUrl(baseURL)
           .addConverterFactory(GsonConverterFactory.create())
           .build();
    
    RetrofitArrayApi service = retrofit.create(RetrofitArrayApi.class);
    Call<List<WPPost>> call= service.getPostInfo(page, pageSize);
    
    call.enqueue(new Callback<List<WPPost>>() {
        @Override
        public void onResponse(Call<List<WPPost>> call, Response<List<WPPost>> response) {
            Log.e("mainactivty","response"+ response.body());
            progressBar.setVisibility(View.GONE);
            
            // 先移除加载中的占位项(如果有的话)
            if (list.size() > 0 && list.get(list.size() - 1).type == Model.LOADING_TYPE) {
                list.remove(list.size() - 1);
                adapter.notifyItemRemoved(list.size());
            }
            
            if (response.body() != null && !response.body().isEmpty()) {
                for(int i=0; i<response.body().size();i++){
                    Log.e ("main","Title"+ response.body().get(i).getTitle().getRendered()+" "+ response.body().get(i).getId());
                    String tempdetails=response.body().get(i).getExcerpt().getRendered().toString();
                    tempdetails=tempdetails.replace("<p>","");
                    tempdetails=tempdetails.replace("</p>","");
                    String linkdetail=response.body().get(i).getLink().toString();
                    String date=response.body().get(i).getDate().toString();
                    list.add(new Model(Model.IMAGE_TYPE, response.body().get(i).getTitle().getRendered(), tempdetails,date, response.body().get(i).getLinks().getWpFeaturedmedia().get(0).getHref(),linkdetail) );
                }
                // 局部刷新,提升性能
                int startPosition = list.size() - response.body().size();
                adapter.notifyItemRangeInserted(startPosition, response.body().size());
            } else {
                // 没有更多数据了,可以给用户添加一个提示占位项
                list.add(new Model(Model.NO_MORE_TYPE, "没有更多数据了", "", "", "", ""));
                adapter.notifyItemInserted(list.size() - 1);
            }
            
            isLoading = false; // 加载完成,重置标记
        }
        
        @Override
        public void onFailure(Call<List<WPPost>> call, Throwable t) {
            Log.e("mainactivity", "加载失败: " + t.getMessage());
            // 移除加载占位项,重置标记
            if (list.size() > 0 && list.get(list.size() - 1).type == Model.LOADING_TYPE) {
                list.remove(list.size() - 1);
                adapter.notifyItemRemoved(list.size());
            }
            isLoading = false;
        }
    });
}

注意:初始化的时候调用getRetrofit(1)来加载第一页数据。


3. 优化Adapter:支持多种布局类型

为了显示加载状态和无数据提示,需要给Adapter添加多布局支持,首先修改Model类,添加对应的类型:

public class Model {
    public static final int IMAGE_TYPE = 0;
    public static final int LOADING_TYPE = 1;
    public static final int NO_MORE_TYPE = 2;
    
    public int type;
    public String title;
    public String subtitle;
    public String date;
    public String imageHref;
    public String link;
    
    // 构造方法也要对应修改
    public Model(int type, String title, String subtitle, String date, String imageHref, String link) {
        this.type = type;
        this.title = title;
        this.subtitle = subtitle;
        this.date = date;
        this.imageHref = imageHref;
        this.link = link;
    }
}

然后修改RecyclerViewAdapter,支持多种ViewType:

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private ArrayList<Model> dataset;
    private Context mContext;
    
    public RecyclerViewAdapter(ArrayList<Model> mlist, Context context) {
        this.dataset=mlist;
        this.mContext=context;
    }
    
    // 加载布局的ViewHolder
    public static class LoadingViewHolder extends RecyclerView.ViewHolder {
        ProgressBar progressBar;
        public LoadingViewHolder(View itemView) {
            super(itemView);
            progressBar = itemView.findViewById(R.id.loading_progress);
        }
    }
    
    // 无更多数据的ViewHolder
    public static class NoMoreViewHolder extends RecyclerView.ViewHolder {
        TextView tipText;
        public NoMoreViewHolder(View itemView) {
            super(itemView);
            tipText = itemView.findViewById(R.id.no_more_tip);
        }
    }
    
    public static class ImageTypeViewHolder extends RecyclerView.ViewHolder{
        TextView title,subtitle,date;
        ImageView imageView;
        public ImageTypeViewHolder(View itemView){
            super(itemView);
            this.title=(TextView)itemView.findViewById(R.id.title);
            this.subtitle=(TextView) itemView.findViewById(R.id.subtitle);
            this.imageView=(ImageView) itemView.findViewById(R.id.icon);
            this.date=(TextView)itemView.findViewById(R.id.date);
        }
    }
    
    @Override
    public int getItemViewType(int position) {
        return dataset.get(position).type;
    }
    
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == Model.LOADING_TYPE) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_loading, parent, false);
            return new LoadingViewHolder(view);
        } else if (viewType == Model.NO_MORE_TYPE) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_no_more, parent, false);
            return new NoMoreViewHolder(view);
        } else {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.postdetails,parent,false);
            return new ImageTypeViewHolder(view);
        }
    }
    
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
        Model object=dataset.get(position);
        if (holder instanceof ImageTypeViewHolder) {
            ((ImageTypeViewHolder) holder).title.setText(object.title);
            ((ImageTypeViewHolder) holder).subtitle.setText(object.subtitle);
            ((ImageTypeViewHolder) holder).date.setText(object.date);
            
            // 建议用Glide/Picasso加载图片,避免OOM
            // Glide.with(mContext)
            //      .load(object.imageHref)
            //      .placeholder(R.drawable.placeholder)
            //      .into(((ImageTypeViewHolder) holder).imageView);
            
            ((ImageTypeViewHolder) holder).title.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Intent intent=new Intent(mContext, WPPostDetails.class);
                    intent.putExtra("itemPosition",position);
                    intent.putExtra("link",object.link.toString());
                    mContext.startActivity(intent);
                }
            });
        } else if (holder instanceof NoMoreViewHolder) {
            ((NoMoreViewHolder) holder).tipText.setText(object.title);
        }
        // 加载布局不需要额外绑定数据
    }
    
    @Override
    public int getItemCount() {
        return dataset.size();
    }
}

你需要新建两个布局文件:

  • item_loading.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="wrap_content"
    android:gravity="center"
    android:padding="16dp">

    <ProgressBar
        android:id="@+id/loading_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>
  • item_no_more.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="wrap_content"
    android:gravity="center"
    android:padding="16dp">

    <TextView
        android:id="@+id/no_more_tip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#999999"
        android:textSize="14sp" />

</LinearLayout>

4. 额外优化建议

  • 图片加载优化:务必用Glide或Picasso加载图片,避免内存溢出,同时支持占位图和错误图。
  • 下拉刷新:可以给RecyclerView套一层SwipeRefreshLayout,支持用户手动刷新第一页数据。
  • 错误重试:加载失败时,可以给用户显示一个重试按钮,重新触发当前页的加载请求。
  • 防抖处理:滚动监听里可以添加延迟判断,避免快速滚动时频繁触发请求。

修改后的MainActivity完整代码

package com.ejobbox.ejobbox;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class MainActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private ProgressBar progressBar;
    private LinearLayoutManager mLayoutManager;
    private RecyclerViewAdapter adapter;
    private ArrayList<Model> list;
    private String baseURL="http://mywebsite.com/";
    public static List<WPPost> mListPost;
    
    // 分页相关变量
    private int currentPage = 1;
    private int pageSize = 10;
    private boolean isLoading = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView= findViewById(R.id.recycler_view);
        progressBar=findViewById(R.id.progressbar);
        mLayoutManager=new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL,false);
        recyclerView.setLayoutManager(mLayoutManager);
        list=new ArrayList<Model>();
        
        // 初始化Adapter
        adapter=new RecyclerViewAdapter(list,MainActivity.this);
        recyclerView.setAdapter(adapter);
        
        // 添加滚动监听
        addScrollListener();
        
        // 加载第一页数据
        getRetrofit(currentPage);
    }
    
    private void addScrollListener() {
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                
                if (dy <= 0) return;
                
                int visibleItemCount = mLayoutManager.getChildCount();
                int totalItemCount = mLayoutManager.getItemCount();
                int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
                
                if (!isLoading && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount) {
                    // 检查最后一项是否是无数据提示,避免重复请求
                    if (list.size() > 0 && list.get(list.size() - 1).type != Model.NO_MORE_TYPE) {
                        currentPage++;
                        getRetrofit(currentPage);
                        isLoading = true;
                        // 添加加载占位项
                        list.add(new Model(Model.LOADING_TYPE, "", "", "", "", ""));
                        adapter.notifyItemInserted(list.size() - 1);
                    }
                }
            }
        });
    }

    private void getRetrofit(int page){
        Retrofit retrofit=new Retrofit.Builder()
               .baseUrl(baseURL)
               .addConverterFactory(GsonConverterFactory.create())
               .build();
        RetrofitArrayApi service = retrofit.create(RetrofitArrayApi.class);
        Call<List<WPPost>> call= service.getPostInfo(page, pageSize);
        call.enqueue(new Callback<List<WPPost>>() {
            @Override
            public void onResponse(Call<List<WPPost>> call, Response<List<WPPost>> response) {
                Log.e("mainactivty","response"+ response.body());
                progressBar.setVisibility(View.GONE);
                
                // 移除加载占位项
                if (list.size() > 0 && list.get(list.size() - 1).type == Model.LOADING_TYPE) {
                    list.remove(list.size() - 1);
                    adapter.notifyItemRemoved(list.size());
                }
                
                if (response.body() != null && !response.body().isEmpty()) {
                    int startSize = list.size();
                    for(int i=0; i<response.body().size();i++){
                        Log.e ("main","Title"+ response.body().get(i).getTitle().getRendered()+" "+ response.body().get(i).getId());
                        String tempdetails=response.body().get(i).getExcerpt().getRendered().toString();
                        tempdetails=tempdetails.replace("<p>","");
                        tempdetails=tempdetails.replace("</p>","");
                        String linkdetail=response.body().get(i).getLink().toString();
                        String date=response.body().get(i).getDate().toString();
                        list.add(new Model(Model.IMAGE_TYPE, response.body().get(i).getTitle().getRendered(), tempdetails,date, response.body().get(i).getLinks().getWpFeaturedmedia().get(0).getHref(),linkdetail) );
                    }
                    // 局部刷新,提升性能
                    adapter.notifyItemRangeInserted(startSize, response.body().size());
                } else {
                    // 添加无更多数据提示
                    list.add(new Model(Model.NO_MORE_TYPE, "没有更多数据了", "", "", "", ""));
                    adapter.notifyItemInserted(list.size() - 1);
                }
                
                isLoading = false;
            }

            @Override
            public void onFailure(Call<List<WPPost>> call, Throwable t) {
                Log.e("mainactivity", "加载失败: " + t.getMessage());
                // 移除加载占位项,重置标记
                if (list.size() > 0 && list.get(list.size() - 1).type == Model.LOADING_TYPE) {
                    list.remove(list.size() - 1);
                    adapter.notifyItemRemoved(list.size());
                }
                isLoading = false;
            }
        });
    }
}

内容的提问来源于stack exchange,提问作者ejobbox

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:10:10