美文网首页android开发杂识Android控件Android知识点和文章分享
SwipeRefreshLayout详解和自定义上拉加载更多

SwipeRefreshLayout详解和自定义上拉加载更多

作者: 笑说余生 | 来源:发表于2016-09-26 19:02 被阅读78565次

个人主页
演示Demo下载


本文重点介绍了SwipeRefreshLayout的使用和自定View继承SwipeRefreshLayout添加上拉加载更多的功能。

  • 介绍之前,先来看一下SwipeRefreshLayout实现的下拉刷新效果图。从图中可以看到,下拉到了一定的高度才会进行刷新,高度不够就会回收上去,正在刷新过程中,继续下拉没反应,说明刷新时屏蔽掉了下拉事件。


    上拉刷新效果图

一、SwipeRefreshLayout简单介绍

  • 先看以下官方文档,已有了很详细的描述了。


    官方文档说明
  • 这里我再大概解释一下:

    • 在竖直滑动时想要刷新页面可以用SwipeRefreshLayout来实现。它通过设置OnRefreshListener来监听界面的滑动从而实现刷新。也可以通过一些方法来设置SwipeRefreshLayout是否可以刷新。如:setRefreshing(true),展开刷新动画。
      setRefreshing(false),取消刷新动画。setEnable(true)下拉刷新将不可用。

    • 使用这个布局要想达到刷新的目的,需要在这个布局里包裹可以滑动的子控件,如ListView等,并且只能有一个子控件。

  • 介绍总结:使用SwipeRefreshLayout可以实现下拉刷新,前提是布局里需要包裹一个可以滑动的子控件,然后在代码里设置OnRefreshListener设置监听,最后在监听里设置刷新时的数据获取就可以了。由于是新出来的东西,所以要想使用,先把support library的版本升级到19.1或更新。

二、SwipeRefreshLayout主要方法介绍

翻看官方的文档,可以看到方法有很多,这里只介绍五个经常用到的方法。

  • isRefreshing()

    • 判断当前的状态是否是刷新状态。
  • setColorSchemeResources(int... colorResIds)

    • 设置下拉进度条的颜色主题,参数为可变参数,并且是资源id,可以设置多种不同的颜色,每转一圈就显示一种颜色。
  • setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener)

    • 设置监听,需要重写onRefresh()方法,顶部下拉时会调用这个方法,在里面实现请求数据的逻辑,设置下拉进度条消失等等。
  • setProgressBackgroundColorSchemeResource(int colorRes)

    • 设置下拉进度条的背景颜色,默认白色。
  • setRefreshing(boolean refreshing)

    • 设置刷新状态,true表示正在刷新,false表示取消刷新。

三、SwipeRefreshLayout的基本使用

  • 介绍了SwipeRefreshLayout,主要的方法也讲了,接下来就是实战,其实使用起来非常的简单。

3.1 设置布局

  • 官方文档已经说明,SwipeRefreshLayout只能有一个孩子,当然我们不般也不会往里面放其他的布局。我们只需要在容器里包裹一个ListView就好了。

              <ListView
                  android:id="@+id/lv"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"/>
      
          </android.support.v4.widget.SwipeRefreshLayout>
      -->
    

3.2 在代码中使用

  • 在该布局文件对应的Activity或其他类中获取布局id,先设置ListView显示的适配器,然后再设置SwipeRefreshLayout。
    // 不能在onCreate中设置,这个表示当前是刷新状态,如果一进来就是刷新状态,SwipeRefreshLayout会屏蔽掉下拉事件
    //swipeRefreshLayout.setRefreshing(true);

      // 设置颜色属性的时候一定要注意是引用了资源文件还是直接设置16进制的颜色,因为都是int值容易搞混
      // 设置下拉进度的背景颜色,默认就是白色的
      swipeRefreshView.setProgressBackgroundColorSchemeResource(android.R.color.white);
      // 设置下拉进度的主题颜色
      swipeRefreshView.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary, R.color.colorPrimaryDark);
    
      // 下拉时触发SwipeRefreshLayout的下拉动画,动画完毕之后就会回调这个方法
      swipeRefreshView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
          @Override
          public void onRefresh() {
    
              // 开始刷新,设置当前为刷新状态
              //swipeRefreshLayout.setRefreshing(true);
    
              // 这里是主线程
              // 一些比较耗时的操作,比如联网获取数据,需要放到子线程去执行
              // TODO 获取数据
              final Random random = new Random();
              new Handler().postDelayed(new Runnable() {
                  @Override
                  public void run() {
                      mList.add(0, "我是天才" + random.nextInt(100) + "号");
                      mAdapter.notifyDataSetChanged();
    
                      Toast.makeText(MainActivity.this, "刷新了一条数据", Toast.LENGTH_SHORT).show();
    
                      // 加载完数据设置为不刷新状态,将下拉进度收起来
                      swipeRefreshView.setRefreshing(false);
                  }
              }, 1200);
    
              // System.out.println(Thread.currentThread().getName());
    
              // 这个不能写在外边,不然会直接收起来
              //swipeRefreshLayout.setRefreshing(false);
          }
      });
    
  • 经过以上两步简单的设置就能使用SwipeRefreshLayout了。

四、自定义View继承SwipeRefreshLayout,添加上拉加载更多功能

由于谷歌并没有提供上拉加载更多的布局,所以我们只能自己去定义布局实现这个功能。

这里通过自定义View继承SwipeRefreshLayout容器,然后添加上拉加载更多的功能。

  • 先来看一下上拉加载更多的效果图


    上拉加载更多效果图

4.1 定义View继承SwipeRefreshLayout,添加上拉加载功能

代码中的注释比较详细,这里就不一一解释了,说一下大概的实现思路,主要分为四步。

4.1.1 获取子控件ListView

  • 在布局使用中,这里和SwipeRefreshLayout一样,ListView是SwipeRefreshView的子控件,所以需要在onLayout()方法中获取子控件ListView。
    // 获取ListView,设置ListView的布局位置
    if (mListView == null) {
    // 判断容器有多少个孩子
    if (getChildCount() > 0) {
    // 判断第一个孩子是不是ListView
    if (getChildAt(0) instanceof ListView) {
    // 创建ListView对象
    mListView = (ListView) getChildAt(0);

                  // 设置ListView的滑动监听
                  setListViewOnScroll();
              }
          }
      }
    

4.1.2 对ListView设置滑动监听

  • 监听ListView的滑动事件,当滑动到底部,并且当前可见页的最后一个条目等于adapter的getCount数目-1,就满足加载数据的条件。
    /**
    * 设置ListView的滑动监听
    */
    private void setListViewOnScroll() {

          mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
              @Override
              public void onScrollStateChanged(AbsListView view, int scrollState) {
                  // 移动过程中判断时候能下拉加载更多
                  if (canLoadMore()) {
                      // 加载数据
                      loadData();
                  }
              }
    
              @Override
              public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    
              }
          });
      }
    

4.1.3 处理SwipeRefreshView容器的分发事件

  • 由于ListView是SwipeRefreshView的子控件,所以这里要进行事件的分发处理,判断用户的滑动距离是否满足条件。
    /**
    * 在分发事件的时候处理子控件的触摸事件
    *
    * @param ev
    * @return
    */
    private float mDownY, mUpY;
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

          switch (ev.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  // 移动的起点
                  mDownY = ev.getY();
                  break;
              case MotionEvent.ACTION_MOVE:
                  // 移动过程中判断时候能下拉加载更多
                  if (canLoadMore()) {
                      // 加载数据
                      loadData();
                  }
    
                  break;
              case MotionEvent.ACTION_UP:
                  // 移动的终点
                  mUpY = getY();
                  break;
          }
          return super.dispatchTouchEvent(ev);
      }
    

4.1.4 判断条件,满足就用回调去加载数据

  • 当满足了需要判断的所有的条件之后,就可以去调用加载数据的方法,这里提供一个设置上拉布局显示和隐藏的方法,通过传入当前的状态,是true就显示加载,是false就隐藏。
    /**
    * 判断是否满足加载更多条件
    *
    * @return
    */
    private boolean canLoadMore() {
    // 1. 是上拉状态
    boolean condition1 = (mDownY - mUpY) >= mScaledTouchSlop;
    if (condition1) {
    System.out.println("是上拉状态");
    }

          // 2. 当前页面可见的item是最后一个条目
          boolean condition2 = false;
          if (mListView != null && mListView.getAdapter() != null) {
              condition2 = mListView.getLastVisiblePosition() == (mListView.getAdapter().getCount() - 1);
          }
    
          if (condition2) {
              System.out.println("是最后一个条目");
          }
          // 3. 正在加载状态
          boolean condition3 = !isLoading;
          if (condition3) {
              System.out.println("不是正在加载状态");
          }
          return condition1 && condition2 && condition3;
      }
    
      /**
       * 处理加载数据的逻辑
       */
      private void loadData() {
          System.out.println("加载数据...");
          if (mOnLoadListener != null) {
              // 设置加载状态,让布局显示出来
              setLoading(true);
              mOnLoadListener.onLoad();
          }
    
      }
    
      /**
       * 设置加载状态,是否加载传入boolean值进行判断
       *
       * @param loading
       */
      public void setLoading(boolean loading) {
          // 修改当前的状态
          isLoading = loading;
          if (isLoading) {
              // 显示布局
              mListView.addFooterView(mFooterView);
          } else {
              // 隐藏布局
              mListView.removeFooterView(mFooterView);
    
              // 重置滑动的坐标
              mDownY = 0;
              mUpY = 0;
          }
      }
    

4.2 使用自定义View

4.2.1. 书写布局

  • 因为是继承自SwipeRefreshLayout,所以SwipeRefreshView也只能有一个孩子

      <!--自定义View实现SwipeRefreshLayout,添加上拉加载更多的功能-->
      <com.pinger.swiperefreshdemo.view.SwipeRefreshView
          android:id="@+id/srl"
          android:layout_width="match_parent"
          android:layout_height="match_parent">
    
          <ListView
              android:id="@+id/lv"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>
    
      </com.pinger.swiperefreshdemo.view.SwipeRefreshView>
    

4.2.2. 在代码中使用

  • 在代码中使用更加的简单,只需要设置监听重写onLoad()方法,在里面加载数据,加载完数据然后设置为不加载状态就可以了。

      // 设置下拉加载更多
      swipeRefreshView.setOnLoadListener(new SwipeRefreshView.OnLoadListener() {
          @Override
          public void onLoad() {
              new Handler().postDelayed(new Runnable() {
                  @Override
                  public void run() {
    
                      // 添加数据
                      for (int i = 30; i < 35; i++) {
                          mList.add("我是天才" + i+ "号");
                          // 这里要放在里面刷新,放在外面会导致刷新的进度条卡住
                          mAdapter.notifyDataSetChanged();
                      }
    
                      Toast.makeText(MainActivity.this, "加载了" + 5 + "条数据", Toast.LENGTH_SHORT).show();
    
                      // 加载完数据设置为不加载状态,将加载进度收起来
                      swipeRefreshView.setLoading(false);
                  }
              }, 1200);
          }
      });
    

个人主页
演示Demo下载

以上纯属于个人平时工作和学习的一些总结分享,如果有什么错误欢迎随时指出,大家可以讨论一起进步。

相关文章

网友评论

  • 14e302696882:有ios版本吗?
  • e3e5895f2b06:setProgressBackgroundColorSchemeResource(int colorRes) 设置透明色时 怎么会有个黑色的渐变的阴影 如何去掉?
  • 彭小铭:楼主666
  • zhaoqy:介绍总结:使用SwipeRefreshLayout可以实现下拉刷新,前提是布局里需要包裹一个可以滑动的子控件。不知楼主有没有测试过,我在SwipeRefreshLayout里面写个TextView也是有下拉效果的。
  • 义手遮天:楼主好!我用的你写的我的控件不够一屏的时候里面嵌套了横向的HorizontalScrollView里面包裹gridView,在gridView里面上下滑动就崩溃.
    报这一行错. return super.dispatchTouchEvent(ev);
  • zhouw:博主 , 为啥我下载了Demo只能下拉刷新, 不能上拉加载?
    刘一尘_3c1b:mSwipeRefreshView.setItemCount(20);//传的值改成14就能上拉了
    c63719aa4d3a:@PingerOne 同样的问题,demo只能下拉刷新,华为P9
    笑说余生:@zhouw 我看看,你什么型号的机子
  • eb3d4963ee34:楼主,下面人给你提的这些问题,你demo里面改了没?
    笑说余生:@小爷18956100462 这个只是当时练习的Demo,肯定会有肯多问题的,有时间我一定看好吧,都是程序员,加班情况你又不是不知道
    eb3d4963ee34:@PingerOne 过了十天了,你看了么?我觉得有很多问题。。。
    笑说余生:@小爷18956100462 最近太忙了没时间,不好意思,这两天看看
  • 水点冰:报错了大神!
    笑说余生: @水点冰 报什么错,你根据日志调试呗
  • 熟人_75b1:首先感谢博主给出思路
    仔细看完博主的代码后,我认为还有些地方可以做小的修改。
    首先我认为onScrollStateChanged这个回调函数中,可以不写
    if (canLoadMore()) {
    // 加载数据
    loadData();
    }
    而是可以增加一个全局变量private boolean mIsLastItem; 并且onScrollStateChanged中改为
    if (i == (mListView.getAdapter().getCount() - 1)) {
    mIsLastItem = true;
    }else {
    mIsLastItem = false;
    }
    然后在dispatchTouchEvent()函数判断事件外部加上一个判断
    if (mIsLastItem && !mIsLoad) {事件判断}
    这样就保证,每次进入事件判断,都是有意义的,不会出现当前在加载,或者还未到list view的最底部就开始一直的去做touch事件判断和处理。
    最后,能够进入到事件判断的话,那么当前是否正在加载,和当前是否已经到底部这两个判断就可以省略了。
    然后就是还有一件事有疑问,现在我们假设开始加载有两种情况,第一种,只要向上滑动的距离超过了阈值,就直接开始加载,不管最后手离开屏幕的时候向上滑动的距离是多少,
    case MotionEvent.ACTION_MOVE:
    // 移动过程中判断时候能下拉加载更多
    if (canLoadMore()) {
    // 加载数据
    loadData();
    }

    break;
    博主的这里符合这种情况。
    第二种情况是,不管中间是怎么滑动的,只管最后手离开屏幕的时候的向上滑动的距离是多少,case MotionEvent.ACTION_UP:
    // 移动的终点
    mUpY = getY();
    break;
    }
    博主的这里是符合第二种情况。
    但是这么写,就感觉有点问题了,因为mUpY是最后离开屏幕才记录的,但是离开了屏幕后ACTION_MOVE又进不去了,好像会出现问题。所以我依据第一种情况,判断touch事件做了如下处理
    if (mIsLastItem && !mIsLoad) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
    mStartY = ev.getY();
    break;
    case MotionEvent.ACTION_MOVE:
    mEndY = ev.getY();
    if ((mEndY - mStartY) >= SCALED_TOUCH_SLOP) {
    loadData();
    }
    break;
    case MotionEvent.ACTION_UP:
    break;
    }
    }

    这样符合我开始说的第一种情况。 当然我是刚刚看完,还没有自己仔细的去测试来验证自己的猜想,如果有不对的地方,还请多指证。
    笑说余生:@itzhy 当前列表item的position啊,如果当前条目是最后一个条目
    0599beed3c0c:if (i == (mListView.getAdapter().getCount() - 1)) 层主,i是哪个?
    笑说余生:看了下,很赞
  • 孤独的追寻着:不实用
    笑说余生: @孤独的追寻着 这只是个Demo,不实用也没让你看!
  • a101bcb6ccce:请问楼主,为什么这个上拉加载更多我换了一个手机,就不好使了,报类型转换异常的错误怎么解决啊?
    mListView.removeFooterView(mFooterView);源代码中的这句话,报转换异常:

    java.lang.ClassCastException: ccbnet.unicom114.fragment.Fragment1$MyOrderOutAdapter cannot be cast to android.widget.HeaderViewListAdapter
    at android.widget.ListView.removeFooterView(ListView.java:397)
    at ccbnet.unicom114.ui.SwipeRefreshView.setLoading(SwipeRefreshView.java:152)
    at ccbnet.unicom114.fragment.Fragment1$2$1.run(Fragment1.java:142)
    笑说余生: @a101bcb6ccce 你看一下xml中包含的子布局是不是ListView,类型转换异常你看一下哪个类型有问题啊,看日志解bug
  • 30301c6f35d7:请问我上滑加载以后再点击ListView中的ViewPager会报错,是我哪里写错了吗:joy:
    笑说余生: @怀旧的NiceBoy 那你得看你报什么错了,看错误日志。。。
  • 11a9bbd37966:代码有两个问题:
    1.第一个孩子不一定是ListView
    修改如下: for (int i = 0; i < size; i++) {
    View view = getChildAt(i);
    if (view instanceof ListView) {
    // 创建ListView对象
    mListView = (ListView) view;
    // 设置ListView的滑动监听
    setListViewOnScroll();
    return;
    }
    }
    2.单击事件也当成了下拉事件用,canLoadMore中增加判断mUpY>0 才能继续下面的逻辑
    贾亦真亦贾:应该是把 mUpY的取值方式也写成 ev.getY() 源码里写的是 getY() 永远是0
    笑说余生: @浮生若梦_aa6e 厉害,赞一个😂
  • 沈敏杰:这个demo用listview做例子,不太好,listView不兼容NestedScrolled机制滚动,文章还是很不错的
    漫步星空下:当ListView的Item没有多到将手机屏幕填满时(Item只有两三条),上拉会触发上拉加载导致footview出现在底部,如何在ListView的Item没有将屏幕填满时并且执行上拉操作屏蔽掉上拉加载事件?
    flow__啊:老哥6666啊,你的博客里全是干货
    笑说余生:@沈敏杰 这是之前做上拉加载和下拉刷新的时候写的,没做那么多的兼容什么的
  • 冰楓紫憶:为什么下拉刷新的时候,也会触发上拉加载。。只想下拉刷新的时候,只有刷新,上拉的时候只有加载 :cry:
    9675bfee63f4:@敲代码的大圣 你有没有解决办法?
    9675bfee63f4:@冰楓紫憶 同问 只有第一次的时候是这样子的
    笑说余生: @冰楓紫憶 下拉的逻辑跟上拉是分开的啊😂
  • 梦想编织者灬小楠:setColorSchemeResources(int... colorResIds)
    不止支持四种颜色,刚刚试过5种、6种都没问题... :cupid:
    笑说余生: @Amanduzhuojiang 不会吧,这个方法只是设置圆圈的颜色的
    40f614d9bb95:@PingerOne setColorSchemeResources他这个属性设置一个参数就变成圆形图标刷新,设置4个参数就变成进度条刷新的吗?感觉很好奇!
    笑说余生: @梦想编制者灬小楠 我自己试了一下确实可以,网上都是瞎说的,我自己也没测试,惭愧,谢谢提醒
  • AmatorLee:楼主我想问问假如listview为空会不会有冲突?等我下载demo认真学习一下🌞
    笑说余生: @AmatorLee 获取容器的孩子时已经判断过listview的各种状态,如果真的是空的话就没有上拉加载的效果,但是下拉刷新的效果不会变的
  • dongjunkun:仅仅支持ListView吗?
    笑说余生: @dongjunkun 就是加强版的listview,一样
    dongjunkun:@敲代码的大圣 现在RecyclerView用的特别多,ListView几乎不用了
    笑说余生: @dongjunkun 目前自定义view里只做个listview的处理,你可以自己去研究一下做其他的,不过一般用到的都是listview

本文标题:SwipeRefreshLayout详解和自定义上拉加载更多

本文链接:https://www.haomeiwen.com/subject/nobqyttx.html