RecyclerView下拉刷新、上拉加载及添加头布局、脚布局实现

image.png

前言


  • 随着RecyclerView的越来越流行,我看着项目里ListView、GridView陷入沉思,是时候开始改变了!(认真脸)我决定将项目中的这些控件都改用RecyclerView。然而,像下拉刷新等功能是必不可少的,虽然有很多现成的可以用,但是,我毅然决定自己动手。

思路


  • 下定决心了,那么接下来就是考虑该怎么实现了。
  • 由于RecyclerView并没有像ListView一样为我们提供方便的addHeaderView()、addFooterView()方法来添加头布局和脚布局(这也是我们要实现的),所以就不能像ListView一样通过添加头布局、脚布局实现下拉刷新、上拉加载更多功能了。
  • 而在RecyclerView里,展示多少条数据,有多少条目,这些,都是由适配器控制的,所以,要想实现以上的功能,就要从适配器入手了。

实现


  • 思路有了,那么接下来就是如何实现了。

  • 首先,自然是新建RLRecyclerView继承RecyclerView,实现构造方法。

  • 接着,重写setAdapter()方法,将传递进来的适配器对象保存下来,实际上设置的是封装好的实现以上功能的适配器。

      @Override
      public void setAdapter(Adapter adapter) {
          // 保存设置的适配器
          mAdapter = adapter;
          // 设置封装的适配器
          innerAdapter = new InsideAdapter();
    
          super.setAdapter(innerAdapter);
    
          // 注册观察者
          mAdapter.registerAdapterDataObserver(mObserver);
      }
    
  • 至于下面的注册观察者,我们晚点再说,接下来就是这个 InsideAdapter,直接以内部类形式定义在RLRecyclerView中,继承RecyclerView.Adapter

       /**
       * 添加了头布局、脚布局、下拉刷新、上拉加载更多功能的适配器类
       */
      class InsideAdapter extends Adapter {
    
          /** 布局类型-刷新布局 */
          private static final int VIEW_TYPE_REFRESH = 0;
          /** 布局类型-头布局 */
          private static final int VIEW_TYPE_HEADER = 1;
          /** 布局类型-普通布局 */
          private static final int VIEW_TYPE_NORMAL = 2;
          /** 布局类型-脚布局 */
          private static final int VIEW_TYPE_FOOTER = 3;
          /** 布局类型-加载更多布局 */
          private static final int VIEW_TYPE_LOADMORE = 4;
    
          @Override
          public int getItemViewType(int position) {
    
              // 重写方法,根据下标判断布局类型
    
              if (isRefresh(position)) {
                  return VIEW_TYPE_REFRESH;
              } else if (isHeader(position)) {
                  return VIEW_TYPE_HEADER;
              } else if (isFooter(position)) {
                  return VIEW_TYPE_FOOTER;
              } else if (isLoadMore(position)) {
                  return VIEW_TYPE_LOADMORE;
              } else {
                  return VIEW_TYPE_NORMAL;
              }
          }
    
          @Override
          public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    
              ViewHolder holder;
    
              // 根据布局类型,返回不同的ViewHolder对象,SimpleViewHolder不做任何操作
              switch (viewType) {
                  case VIEW_TYPE_REFRESH:
                      holder = new SimpleViewHolder(mRefresh);
                      break;
                  case VIEW_TYPE_HEADER:
                      holder = new SimpleViewHolder(mHeaders.get(headerPosition++));
                      break;
                  case VIEW_TYPE_NORMAL: // 普通布局类型返回设置的Adpter的ViewHolder对象
                      holder = mAdapter.onCreateViewHolder(parent, viewType);
                      break;
                  case VIEW_TYPE_FOOTER:
                      holder = new SimpleViewHolder(mFooters.get(footerPosition++));
                      break;
                  case VIEW_TYPE_LOADMORE:
                      holder = new SimpleViewHolder(mLoadMore);
                      break;
                  default:
                      holder = new SimpleViewHolder(null);
                      break;
              }
    
              return holder;
          }
    
          @Override
          public void onBindViewHolder(ViewHolder holder, int position) {
    
              // 刷新布局、加载更多、头布局、脚布局不做处理
              if (isRefresh(position) || isLoadMore(position)
                      || isHeader(position) || isFooter(position)) {
                  return;
              }
    
              mAdapter.onBindViewHolder(holder, realPosition(position));
          }
    
          @Override
          public int getItemCount() {
    
              // 根据功能开启情况以及头布局脚布局返回实际的条目数
              if (REFRESH_MODE_BOTH.equals(mode)) {
                  return mAdapter.getItemCount() + mHeaders.size() + mFooters.size() + 2;
              } else if (REFRESH_MODE_REFRESH.equals(mode)
                      || REFRESH_MODE_LOADMORE.equals(mode)) {
                  return mAdapter.getItemCount() + mHeaders.size() + mFooters.size() + 1;
              } else {
                  return mAdapter.getItemCount() + mHeaders.size() + mFooters.size();
              }
          }
    
          class SimpleViewHolder extends RecyclerView.ViewHolder {
              SimpleViewHolder(View itemView) {
                  super(itemView);
              }
          }
      }
    
  • 先不说下拉刷新、上拉加载控件需要隐藏,实际运行起来,你会发现,如果使用 LinnerLayoutManager 是没有问题的,但是如果使用的是 GridLayoutManager 或者是 StaggeredGridLayoutManager 你就会发现并没有达到想象中的效果,这是因为我们的代码中实际上只是在设置适配器的时候,添加了几条数据,并没有改变他的展示效果。而 RecyclerView 把布局展示的工作都交给了 LayoutManager,所以这个时候,为了能够实现头布局等能在 GridLayoutManager 和 StaggeredGridLayoutManager 下宽度也能MATCH_PARENT,我们就需要在 InsideAdapter 中重写 onAttachedToRecyclerView 和 onViewAttachedToWindow 两个方法:

      @Override
      public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    
          RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
          if (manager instanceof GridLayoutManager) { // 如果是Grid布局
              final GridLayoutManager gridManager = ((GridLayoutManager) manager);
              gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                  @Override
                  public int getSpanSize(int position) { // 这个方法是返回当前对象所在行有几列
                      return (isRefresh(position) || isLoadMore(position)
                              || isHeader(position) || isFooter(position))
                              ? gridManager.getSpanCount() : 1; // 如果是刷新、加载更多或头布局、脚布局独占一行,否则按照设置展示
                  }
              });
          }
      }
    
      @Override
      public void onViewAttachedToWindow(ViewHolder holder) {
    
          ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
          if (lp != null
                  && lp instanceof StaggeredGridLayoutManager.LayoutParams
                  && (isRefresh(holder.getLayoutPosition()) || isLoadMore(holder.getLayoutPosition())
                  || isHeader(holder.getLayoutPosition()) || isFooter(holder.getLayoutPosition()))) {
              StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
              // 如果是刷新、加载更多或头布局、脚布局独占一行,否则按照设置展示
              p.setFullSpan(true); // 设置独占一行
          }
      }
    
  • 这样,即使布局为 GridLayoutManager 或者 StaggeredGridLayoutManager 刷新、加载更多、头布局、脚布局都是单独的一行了。

  • 接下来要实现的就是下拉刷新、上拉加载更多的功能了,这个实现思路其实和ListView下拉刷新是一致的,同样通过设置刷新布局和加载更多布局的margin值来实现,其核心就是设置触摸事件监听,然后在 onTouch 方法中判断不同的情况,做不同的处理。由于这部分内容比较复杂,笔者就不在这里细说了,有兴趣的朋友可以在文章最后找到Github地址查看源码,源码里都有详细注释。

  • 在前文有说明一段代码后面解释,那就是 mAdapter.registerAdapterDataObserver(mObserver)

  • 这是给使用者设置适配器的时候同时给这个适配器注册了一个观察者:

    private final RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
      @Override
      public void onChanged() {
          innerAdapter.notifyDataSetChanged();
      }
    
      @Override
      public void onItemRangeInserted(int positionStart, int itemCount) {
          innerAdapter.notifyItemRangeInserted(positionStart, itemCount);
      }
    
      @Override
      public void onItemRangeChanged(int positionStart, int itemCount) {
          innerAdapter.notifyItemRangeChanged(positionStart, itemCount);
      }
    
      @Override
      public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
          innerAdapter.notifyItemRangeChanged(positionStart, itemCount, payload);
      }
    
      @Override
      public void onItemRangeRemoved(int positionStart, int itemCount) {
          innerAdapter.notifyItemRangeRemoved(positionStart, itemCount);
      }
    
      @Override
      public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
          innerAdapter.notifyItemMoved(fromPosition, toPosition);
      }
    };
    
  • 这个观察者所做的就是在使用者调用适配器的notifyDataSetChanged方法时,同步调用InnerAdapter的方法,因为通过setAdapter方法设置的适配器实际上是我们封装的InnerAdapter,所以,当数据变更时,需要调用InnerAdapter的方法才能同步更新界面。

总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容