摘要

一直在用到RecyclerView时都会微微一颤,因为一直都没去了解怎么实现上拉加载,受够了每次去Github找开源引入,因为感觉就为了一个上拉加载功能而去引入一大堆你不知道有多少BUG的代码,不仅增加了项目的冗余程度,而且出现BUG的时候,你却发现很难去改,正因为这样,我就下定决心去了解如何来实现RecyclerView的上拉加载功能,相信大家和我有过同样的情况,但是我相信,只要你给自己几分钟看完这篇文章,你就会发现实现一个上拉加载是非常的简单
 

什么是上拉加载

上拉加载和下拉刷新相对应,在Android API LEVEL 19(即4.4)之后,Google官方推出了SwipeRefreshLayout和RecyclerView的共同使用,为我们提供了更加便捷的列表下拉刷新功能,但是,并没有给我们提供上拉加载功能,但是在RecyclerView强大的可扩展之下,Github上面有了很多开源项目实现了上拉加载功能,即我们不会一次性将所有数据加载到列表中,当用户滑动到底部时,再向服务器请求数据,再填充数据到列表中,这样不仅可以有更好的人机交互,同时在减少了服务器的压力的同时也对客户端的性能有了更好的提升。本篇文章主要通过介绍实现以下简单的上拉加载功能,同学们可以在掌握了最基本的实现功能之后,再通过扩展和优化,甚至可以封装成比较通用的代码,开源到Github上面。
 

实现思路

 

一、XML的实现

 
布局很简单,只有一个SwipeRefreshLayout包裹了一个RecyclerView,相信用过RecyclerView的都很容易看懂。如下为activity_main.xml:
 
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent"
  4. android:orientation="vertical">
  5. <android.support.v4.widget.SwipeRefreshLayout
  6. android:id="@+id/refreshLayout"
  7. android:layout_width="match_parent"
  8. android:layout_height="match_parent">
  9. <android.support.v7.widget.RecyclerView
  10. android:id="@+id/recyclerView"
  11. android:layout_width="match_parent"
  12. android:layout_height="match_parent"/>
  13. </android.support.v4.widget.SwipeRefreshLayout>
  14. </LinearLayout>
 
然后,我们RecyclerView的Item布局也是非常简单,只有一个TextView。如下为item.xml:
 
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="wrap_content"
  4. android:orientation="vertical">
  5. <TextView
  6. android:id="@+id/tv"
  7. android:layout_width="match_parent"
  8. android:layout_height="120dp"
  9. android:background="@android:color/holo_blue_dark"
  10. android:gravity="center"
  11. android:textSize="30sp"
  12. android:textColor="#ffffff"
  13. android:text="11"
  14. android:layout_marginBottom="1dp"/>
  15. </LinearLayout>
 

看到我们效果图都知道,在我们上拉时,还有一个提示的条目,我定义为 footview.xml:

 
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="wrap_content"
  4. android:orientation="vertical">
  5. <TextView
  6. android:id="@+id/tips"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:gravity="center"
  10. android:padding="30dp"
  11. android:textSize="15sp"
  12. android:layout_marginBottom="1dp"/>
  13. </LinearLayout>

二、初始化SwipeRefreshLayout

 
在准备好了布局文件之后,我们就把目光转到Activity中去,首先我们需要初始化SwipeRefreshLayout,初始化也是很简单,这里省去了findView操作,所以就只有设置转动的颜色,还有设置刷新的监听事件:
 
  1. private void initRefreshLayout() {
  2. refreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,
  3. android.R.color.holo_orange_light, android.R.color.holo_green_light);
  4. refreshLayout.setOnRefreshListener(this);
  5. }
  6. @Override
  7. public void onRefresh() {
  8. // 设置可见
  9. refreshLayout.setRefreshing(true);
  10. // 重置adapter的数据源为空
  11. adapter.resetDatas();
  12. // 获取第第0条到第PAGE_COUNT(值为10)条的数据
  13. updateRecyclerView(0, PAGE_COUNT);
  14. mHandler.postDelayed(new Runnable() {
  15. @Override
  16. public void run() {
  17. // 模拟网络加载时间,设置不可见
  18. refreshLayout.setRefreshing(false);
  19. }
  20. }, 1000);
  21. }

三、定义RecyclerView的Adapter

 
  1. public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
  2. private List<String> datas; // 数据源
  3. private Context context;    // 上下文Context
  4. private int normalType = 0;     // 第一种ViewType,正常的item
  5. private int footType = 1;       // 第二种ViewType,底部的提示View
  6. private boolean hasMore = true;   // 变量,是否有更多数据
  7. private boolean fadeTips = false; // 变量,是否隐藏了底部的提示
  8. private Handler mHandler = new Handler(Looper.getMainLooper()); //获取主线程的Handler
  9. public MyAdapter(List<String> datas, Context context, boolean hasMore) {
  10. // 初始化变量
  11. this.datas = datas;
  12. this.context = context;
  13. this.hasMore = hasMore;
  14. }
  15. // 获取条目数量,之所以要加1是因为增加了一条footView
  16. @Override
  17. public int getItemCount() {
  18. return datas.size() + 1;
  19. }
  20. // 自定义方法,获取列表中数据源的最后一个位置,比getItemCount少1,因为不计上footView
  21. public int getRealLastPosition() {
  22. return datas.size();
  23. }
  24. // 根据条目位置返回ViewType,以供onCreateViewHolder方法内获取不同的Holder
  25. @Override
  26. public int getItemViewType(int position) {
  27. if (position == getItemCount() - 1) {
  28. return footType;
  29. } else {
  30. return normalType;
  31. }
  32. }
  33. // 正常item的ViewHolder,用以缓存findView操作
  34. class NormalHolder extends RecyclerView.ViewHolder {
  35. private TextView textView;
  36. public NormalHolder(View itemView) {
  37. super(itemView);
  38. textView = (TextView) itemView.findViewById(R.id.tv);
  39. }
  40. }
  41. // // 底部footView的ViewHolder,用以缓存findView操作
  42. class FootHolder extends RecyclerView.ViewHolder {
  43. private TextView tips;
  44. public FootHolder(View itemView) {
  45. super(itemView);
  46. tips = (TextView) itemView.findViewById(R.id.tips);
  47. }
  48. }
  49. @Override
  50. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  51. // 根据返回的ViewType,绑定不同的布局文件,这里只有两种
  52. if (viewType == normalType) {
  53. return new NormalHolder(LayoutInflater.from(context).inflate(R.layout.item, null));
  54. } else {
  55. return new FootHolder(LayoutInflater.from(context).inflate(R.layout.footview, null));
  56. }
  57. }
  58. @Override
  59. public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
  60. // 如果是正常的imte,直接设置TextView的值
  61. if (holder instanceof NormalHolder) {
  62. ((NormalHolder) holder).textView.setText(datas.get(position));
  63. } else {
  64. // 之所以要设置可见,是因为我在没有更多数据时会隐藏了这个footView
  65. ((FootHolder) holder).tips.setVisibility(View.VISIBLE);
  66. // 只有获取数据为空时,hasMore为false,所以当我们拉到底部时基本都会首先显示“正在加载更多...”
  67. if (hasMore == true) {
  68. // 不隐藏footView提示
  69. fadeTips = false;
  70. if (datas.size() > 0) {
  71. // 如果查询数据发现增加之后,就显示正在加载更多
  72. ((FootHolder) holder).tips.setText("正在加载更多...");
  73. }
  74. } else {
  75. if (datas.size() > 0) {
  76. // 如果查询数据发现并没有增加时,就显示没有更多数据了
  77. ((FootHolder) holder).tips.setText("没有更多数据了");
  78. // 然后通过延时加载模拟网络请求的时间,在500ms后执行
  79. mHandler.postDelayed(new Runnable() {
  80. @Override
  81. public void run() {
  82. // 隐藏提示条
  83. ((FootHolder) holder).tips.setVisibility(View.GONE);
  84. // 将fadeTips设置true
  85. fadeTips = true;
  86. // hasMore设为true是为了让再次拉到底时,会先显示正在加载更多
  87. hasMore = true;
  88. }
  89. }, 500);
  90. }
  91. }
  92. }
  93. }
  94. // 暴露接口,改变fadeTips的方法
  95. public boolean isFadeTips() {
  96. return fadeTips;
  97. }
  98. // 暴露接口,下拉刷新时,通过暴露方法将数据源置为空
  99. public void resetDatas() {
  100. datas = new ArrayList<>();
  101. }
  102. // 暴露接口,更新数据源,并修改hasMore的值,如果有增加数据,hasMore为true,否则为false
  103. public void updateList(List<String> newDatas, boolean hasMore) {
  104. // 在原有的数据之上增加新数据
  105. if (newDatas != null) {
  106. datas.addAll(newDatas);
  107. }
  108. this.hasMore = hasMore;
  109. notifyDataSetChanged();
  110. }
  111. }

四、初始化RecyclerView

 
  1. private void initRecyclerView() {
  2. // 初始化RecyclerView的Adapter
  3. // 第一个参数为数据,上拉加载的原理就是分页,所以我设置常量PAGE_COUNT=10,即每次加载10个数据
  4. // 第二个参数为Context
  5. // 第三个参数为hasMore,是否有新数据
  6. adapter = new MyAdapter(getDatas(0, PAGE_COUNT), this, getDatas(0, PAGE_COUNT).size() > 0 ? true : false);
  7. mLayoutManager = new GridLayoutManager(this, 1);
  8. recyclerView.setLayoutManager(mLayoutManager);
  9. recyclerView.setAdapter(adapter);
  10. recyclerView.setItemAnimator(new DefaultItemAnimator());
  11. // 实现上拉加载重要步骤,设置滑动监听器,RecyclerView自带的ScrollListener
  12. recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  13. @Override
  14. public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
  15. super.onScrollStateChanged(recyclerView, newState);
  16. // 在newState为滑到底部时
  17. if (newState == RecyclerView.SCROLL_STATE_IDLE) {
  18. // 如果没有隐藏footView,那么最后一个条目的位置就比我们的getItemCount少1,自己可以算一下
  19. if (adapter.isFadeTips() == false && lastVisibleItem + 1 == adapter.getItemCount()) {
  20. mHandler.postDelayed(new Runnable() {
  21. @Override
  22. public void run() {
  23. // 然后调用updateRecyclerview方法更新RecyclerView
  24. updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
  25. }
  26. }, 500);
  27. }
  28. // 如果隐藏了提示条,我们又上拉加载时,那么最后一个条目就要比getItemCount要少2
  29. if (adapter.isFadeTips() == true && lastVisibleItem + 2 == adapter.getItemCount()) {
  30. mHandler.postDelayed(new Runnable() {
  31. @Override
  32. public void run() {
  33. // 然后调用updateRecyclerview方法更新RecyclerView
  34. updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
  35. }
  36. }, 500);
  37. }
  38. }
  39. }
  40. @Override
  41. public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  42. super.onScrolled(recyclerView, dx, dy);
  43. // 在滑动完成后,拿到最后一个可见的item的位置
  44. lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
  45. }
  46. });
  47. }
  48. // 上拉加载时调用的更新RecyclerView的方法
  49. private void updateRecyclerView(int fromIndex, int toIndex) {
  50. // 获取从fromIndex到toIndex的数据
  51. List<String> newDatas = getDatas(fromIndex, toIndex);
  52. if (newDatas.size() > 0) {
  53. // 然后传给Adapter,并设置hasMore为true
  54. adapter.updateList(newDatas, true);
  55. } else {
  56. adapter.updateList(null, false);
  57. }
  58. }

所以,Activity的完整代码如下:

 
  1. public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
  2. private SwipeRefreshLayout refreshLayout;
  3. private RecyclerView recyclerView;
  4. private List<String> list;
  5. private int lastVisibleItem = 0;
  6. private final int PAGE_COUNT = 10;
  7. private GridLayoutManager mLayoutManager;
  8. private MyAdapter adapter;
  9. private Handler mHandler = new Handler(Looper.getMainLooper());
  10. @Override
  11. protected void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. setContentView(R.layout.activity_main);
  14. initData();
  15. findView();
  16. initRefreshLayout();
  17. initRecyclerView();
  18. }
  19. private void initData() {
  20. list = new ArrayList<>();
  21. for (int i = 1; i <= 40; i++) {
  22. list.add("条目" + i);
  23. }
  24. }
  25. private void findView() {
  26. refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout);
  27. recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
  28. }
  29. private void initRefreshLayout() {
  30. refreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,
  31. android.R.color.holo_orange_light, android.R.color.holo_green_light);
  32. refreshLayout.setOnRefreshListener(this);
  33. }
  34. private void initRecyclerView() {
  35. adapter = new MyAdapter(getDatas(0, PAGE_COUNT), this, getDatas(0, PAGE_COUNT).size() > 0 ? true : false);
  36. mLayoutManager = new GridLayoutManager(this, 1);
  37. recyclerView.setLayoutManager(mLayoutManager);
  38. recyclerView.setAdapter(adapter);
  39. recyclerView.setItemAnimator(new DefaultItemAnimator());
  40. recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  41. @Override
  42. public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
  43. super.onScrollStateChanged(recyclerView, newState);
  44. if (newState == RecyclerView.SCROLL_STATE_IDLE) {
  45. if (adapter.isFadeTips() == false && lastVisibleItem + 1 == adapter.getItemCount()) {
  46. mHandler.postDelayed(new Runnable() {
  47. @Override
  48. public void run() {
  49. updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
  50. }
  51. }, 500);
  52. }
  53. if (adapter.isFadeTips() == true && lastVisibleItem + 2 == adapter.getItemCount()) {
  54. mHandler.postDelayed(new Runnable() {
  55. @Override
  56. public void run() {
  57. updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
  58. }
  59. }, 500);
  60. }
  61. }
  62. }
  63. @Override
  64. public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  65. super.onScrolled(recyclerView, dx, dy);
  66. lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
  67. }
  68. });
  69. }
  70. private List<String> getDatas(final int firstIndex, final int lastIndex) {
  71. List<String> resList = new ArrayList<>();
  72. for (int i = firstIndex; i < lastIndex; i++) {
  73. if (i < list.size()) {
  74. resList.add(list.get(i));
  75. }
  76. }
  77. return resList;
  78. }
  79. private void updateRecyclerView(int fromIndex, int toIndex) {
  80. List<String> newDatas = getDatas(fromIndex, toIndex);
  81. if (newDatas.size() > 0) {
  82. adapter.updateList(newDatas, true);
  83. } else {
  84. adapter.updateList(null, false);
  85. }
  86. }
  87. @Override
  88. public void onRefresh() {
  89. refreshLayout.setRefreshing(true);
  90. adapter.resetDatas();
  91. updateRecyclerView(0, PAGE_COUNT);
  92. mHandler.postDelayed(new Runnable() {
  93. @Override
  94. public void run() {
  95. refreshLayout.setRefreshing(false);
  96. }
  97. }, 1000);
  98. }
  99. }

后话

以上代码我是考虑到了更多的边界条件,所以在代码上会稍微多了一点,但是也不影响观看。大家也可以通过改变数据源的数量和PAGE_COUNT等来测试,每个人在具体使用上都会有不同的要求,所以基本代码我摆了出来,众口难调,更多的细节需要大家来优化,例如footView可以设置一个动画条,下拉刷新用其他样式替换原生的样式等,我想,这些对于学习完这篇文章的你来说,都会是简单的问题了。

Demo下载

Github下载:PullToLoadData-RecyclerView

CSDN资源:PullToLoadData-RecyclerView

源码地址:https://github.com/ryanlijianchang/PullToLoadData-RecyclerView

手把手教你实现Android RecyclerView上拉加载功能的更多相关文章

  1. 手把手教你轻松实现listview上拉加载

    上篇讲了如何简单快速的的实现listview下拉刷新,那么本篇将讲解如何简单快速的实现上拉加载更多.其实,如果你已经理解了下拉刷新的实现过程,那么实现上拉加载更多将变得轻松起来,原理完全一致,甚至实现 ...

  2. RecyclerViewLoadMoreDemo【封装上拉加载功能的RecyclerView,搭配SwipeRefreshLayout实现下拉刷新】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 封装含有上拉加载功能的RecyclerView,然后搭配SwipeRefreshLayout实现下拉刷新.上拉加载功能. 在项目中将 ...

  3. RecyclerView 上拉加载下拉刷新

    RecyclerView 上拉加载下拉刷新 <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/teach_s ...

  4. android ListView上拉加载更多 下拉刷新功能实现(采用pull-to-refresh)

    Android实现上拉加载更多功能以及下拉刷新功能, 采用了目前比较火的PullToRefresh,他是目前实现比较好的下拉刷新的类库. 目前他支持的控件有:ListView, ExpandableL ...

  5. 下拉刷新,上拉加载功能--dropload.js的使用

    这段时间工作太忙了,没时间更新博客内容,在这段时间,也学习到了不少新的知识.今天先整理一下dropload.js的使用方法吧,这个是在为项目中使用过的插件,很好用,但是真正用到项目中还是会有一些小小的 ...

  6. vux-scroller实现移动端上拉加载功能

    本文将讲述vue-cli+vux-scroller实现移动端的上拉加载功能: 纠错声明:网上查阅资料看到很多人都将vux和vuex弄混,在这里我们先解释一下,vuex是vue框架自带的组件,是数据状态 ...

  7. Android 开发 上拉加载更多功能实现

    实现思维 开始之前先废话几句,Android系统没有提供上拉加载的控件,只提供了下拉刷新的SwipeRefreshLayout控件.这个控件我们就不废话,无法实现上拉刷新的功能.现在我们说说上拉加载更 ...

  8. android 支持上拉加载,下拉刷新的列表控件SwipeRefreshLayout的二次封装

    上拉加载,下拉刷新的列表控件,大家一定都封装过,或者使用过 源代码,我会在最后贴出来 这篇代码主要是为了解决两个问题 1.滑动冲突得问题 2.listview无数据时,无数据布局的展示问题 下方列出的 ...

  9. ionic1 下拉刷新 上拉加载 功能

    html页面如下 <ion-content> <ion-refresher pulling-text="刷新" on-refresh="search() ...

随机推荐

  1. PyQt:无边框自定义标题栏及最大化最小化窗体大小调整

    环境 Python3.5.2 PyQt5 陈述 隐藏掉系统的控制栏,实现了自定义的标题控制栏,以及关闭/最大化/最小化的功能,自由调整窗体大小的功能(跟随一个大佬学的),代码内有详细注释 只要把Mai ...

  2. 计蒜客:Entertainment Box

    Ada, Bertrand and Charles often argue over which TV shows to watch, and to avoid some of their fight ...

  3. 在Java中使用redisTemplate操作缓存

    背景 在最近的项目中,有一个需求是对一个很大的数据库进行查询,数据量大概在几千万条.但同时对查询速度的要求也比较高. 这个数据库之前在没有使用Presto的情况下,使用的是Hive,使用Hive进行一 ...

  4. C语言实现二叉树中统计叶子结点的个数&度为1&度为2的结点个数

    算法思想 统计二叉树中叶子结点的个数和度为1.度为2的结点个数,因此可以参照二叉树三种遍历算法(先序.中序.后序)中的任何一种去完成,只需将访问操作具体变为判断是否为叶子结点和度为1.度为2的结点及统 ...

  5. leetcode — word-ladder-ii

    import java.util.*; /** * Source : https://oj.leetcode.com/problems/word-ladder-ii/ * * * Given two ...

  6. 基于 Nginx 的 HTTPS 性能优化

    前言 分享一个卓见云的较多客户遇到HTTPS优化案例. 随着相关浏览器对HTTP协议的“不安全”.红色页面警告等严格措施的出台,以及向 iOS 应用的 ATS 要求和微信.支付宝小程序强制 HTTPS ...

  7. [二] java8 函数式接口详解 函数接口详解 lambda表达式 匿名函数 方法引用使用含义 函数式接口实例 如何定义函数式接口

    函数式接口详细定义 package java.lang; import java.lang.annotation.*; /** * An informative annotation type use ...

  8. Mybatis学习(七)————— mybatis的逆向工程的配置详解

    一.什么是逆向工程? 简单点说,就是通过数据库中的单表,自动生成java代码. Mybatis官方提供了逆向工程,可以针对单表自动生成mybatis代码(mapper.java\mapper.xml\ ...

  9. Linux和Shell回炉复习系列文章总目录

    本页内容都是本人回炉Linux时整理出来的.这些文章中,绝大多数命令类内容都是翻译.整理man或info文档总结出来的,所以相对都比较完整. 本人的写作方式.风格也可能会让朋友一看就恶心到直接右上角叉 ...

  10. Perl获取主机名、用户、组、网络信息

    获取主机名.用户.组.网络信息相关函数 首先是获取主机名的方式,Perl提供了Sys::Hostname模块,可以查询当前的主机名: use Sys::Hostname; print hostname ...