RecyclerView+SwipeRefreshLayou封装下拉刷新,上拉加载功能

随着Android系统的进步,我们不该老有老旧的ListView来封装下拉刷新,上拉加载了,今天记录一下如何用RecyclerView+SwipeRefreshLayou 封装下拉刷新上拉加载。

Android很奇怪。。以前是没有提供瀑布流,刷新控件等。现在是提供了刷新的SwipeRefreshLayou 但又没提供加载更多的功能╮(╯▽╰)╭。总是要劳烦大家自己弄出五花八门的东西来。而且这个SwipeRefreshLayou 和RecyclerView封装下拉刷新还会出现一些问题。比如:

1、手动调用SwipeRefreshLayou 的setRefresh(true)方法,是可以显示刷新动画,但并不会回调onRefresh()方法。

2、假定已经封装好了,写一个方法供外部调用,内部手动调用setRefresh(true)并且手动调用onRefresh()方法,其他情况都好用,但在首次进入页面的时候,会发现刷新动画不显示。

3、我们实现加载更多的思路是:当滚动到最后一个Item的时候,再就可以加载更多了。但是RecyclerView的布局管理器如何获取到这个情况呢?

4、瀑布流不规则,如何在最后一行显示一个充满屏幕宽度的加载更多布局?

先来看一下效果:

 

用户体验非常的好,这只是简单的演示版本,成品程序更完善。

这里来贴一下主要实现类的代码:

 

RefreshLoadingRecyclerView.java   封装SwipeRefreshLayout与RecyclerView的类,以及主要逻辑:
 
/** * Created by Ade-rui on 2016/12/19. */ public class RefreshLoadingRecyclerView extends FrameLayout implements SwipeRefreshLayout.OnRefreshListener{ /** * 空闲状态 */ public static final int STATE_IDLE = 1002; /** * 加载更多中 */ public static final int STATE_LOAD_MORE = 1003; /** * 刷新中 */ public static final int STATE_REFRESH = 1004; /** * 刷新器 */ private SwipeRefreshLayout swipeRefreshLayout; /** * recyclerview */ private RecyclerView recyclerView; /** * 刷新与加载的回调监听 */ private OnRefreshLoadingListener listener; /** * 布局管理器 */ private ILayoutManager layoutManager; /** * 适配器 */ private BaseRecyclerAdapter adapter; /** * 当前状态 * 默认为空闲 */ private int mCurrentState = STATE_IDLE; /** * 是否开启加载更多功能 */ private boolean isEnableLoadMore; public RefreshLoadingRecyclerView(Context context) { super(context); init(); } public RefreshLoadingRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { LayoutInflater.from(getContext()).inflate(R.layout.layout_recycler_view,this,true); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout); recyclerView = (RecyclerView) findViewById(R.id.recycler_view); swipeRefreshLayout.setOnRefreshListener(this); //设置recyclerView默认的LayoutManager -- > 相当于普通listview样式 this.layoutManager = new MyLinearLayoutManager(getContext()); recyclerView.setLayoutManager(layoutManager.getLayoutManager()); recyclerView.addOnScrollListener(onScrollListener); } /** * 滚动监听 */ private RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); /** * mCurrentState == STATE_IDLE 因为如果是加载中的话,继续有下拉的动作,还会回调加载 * ,所以必须是空闲状态下才能回调加载。进入加载回调就立刻将状态修改为加载状态,避免再次回聊。 * isEnableLoadMore : 用来手动设置是否开启加载更多的功能 * loadingSecurity() :判断是否可以加载更多,如何实现由具体的布局管理器而定 * dy>0 : 为了返回有时候数据不满一屏,都没有滚动,便发生了加载的情况 */ if(mCurrentState == STATE_IDLE&&isEnableLoadMore && loadingSecurity() && dy > 0){ loadMore(); } } }; /** * 设置是否开启加载更多的功能 * @param isEnableLoadMore */ public void isEnableLoadMore(boolean isEnableLoadMore){ this.isEnableLoadMore = isEnableLoadMore; } /** * 加载更多 */ private void loadMore() { if(listener == null){ return; } //修改状态 mCurrentState = STATE_LOAD_MORE; //禁止swipeRefreshLayout功能 swipeRefreshLayout.setEnabled(false); //通知adapter显示加载更多的布局 adapter.loadMoreChangeState(true); listener.onLoading(); } /** * 完成加载或者刷新 * 由用户调用 */ public void complete(){ if(mCurrentState == STATE_LOAD_MORE){ //通知adapter隐藏加载更多布局 adapter.loadMoreChangeState(false); //回复SwipeRefreshLayout功能 swipeRefreshLayout.setEnabled(true); }else if(mCurrentState == STATE_REFRESH){ //让刷新动画消失 swipeRefreshLayout.setRefreshing(false); } mCurrentState = STATE_IDLE; } /** * 是否可以加载 * 根据具体的LayoutManager实现来判断 * @return */ private boolean loadingSecurity() { return layoutManager.loadMore(); } /** * 设置适配器 * @param adapter */ public void setAdapter(BaseRecyclerAdapter adapter){ this.adapter = adapter; recyclerView.setAdapter(adapter); } /** * 设置布局管理器 * @param layoutManager */ public void setLayoutManager(ILayoutManager layoutManager){ this.layoutManager = layoutManager; recyclerView.setLayoutManager(layoutManager.getLayoutManager()); } /** * 设置刷新加载监听 * @param listener */ public void setOnRefreshLoadingListener(OnRefreshLoadingListener listener){ this.listener = listener; } /** * 刷新 */ public void setRefresh(){ /** * 使用post加入消息队列调用---》 可以防止第一次进入页面的时候, * 虽然执行swipeRefreshLayout.setRefreshing(true),但并不会显示刷新动画的bug */ post(new Runnable() { @Override public void run() { swipeRefreshLayout.setRefreshing(true); onRefresh(); } }); } /** * RereshLayout监听到下拉手势的回调,然后我们在这个回调里面修改自身控件的状态 * 并且回调总结的listener给用户 */ @Override public void onRefresh() { if(listener!=null){ mCurrentState = STATE_REFRESH; Log.i("mydata","onRefresh"); listener.onRefresh(); } } /** * 监听回调 */ public interface OnRefreshLoadingListener{ void onRefresh(); void onLoading(); } }

 

 

 

 

短短的200行代码便可以实现这个功能了。在XML文件声明以及在Activity中的使用:

 

 

 
 
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <drawapp.cw.com.recyclerviewdemo.RefreshLoadingRecyclerView android:id="@+id/pull_view" android:layout_width="match_parent" android:layout_height="match_parent"></drawapp.cw.com.recyclerviewdemo.RefreshLoadingRecyclerView> </RelativeLayout>


Activity:

 
pullView = (RefreshLoadingRecyclerView) findViewById(R.id.pull_view); adapter = new BaseRecyclerAdapter(new ArrayList<String>()); pullView.setAdapter(adapter); pullView.setOnRefreshLoadingListener(new RefreshLoadingRecyclerView.OnRefreshLoadingListener() { @Override public void onRefresh() { refresh(); } @Override public void onLoading() { load(); } }); pullView.isEnableLoadMore(true); pullView.setRefresh();


使用起来与流行的第三方库等没有任何差别。

 
现在来统一总结一下都需要解决哪些问题:
 
 

1、手动调用SwipeRefreshLayou 的setRefresh(true)方法,是可以显示刷新动画,但并不会回调onRefresh()方法。

解决方法与2是一体的,向外提供一个setRefresh()方法,外部调用的时候,内部同时调用显示动画和onRefresh方法。

2、假定已经封装好了,写一个方法供外部调用,内部手动调用setRefresh(true)并且手动调用onRefresh()方法,其他情况都好用,但在首次进入页面的时候,会发现刷新动画不显示。

在内部使用post加入消息队列中使用,在第一次进入页面就也可以看到刷新动画了

 post(new Runnable() {
            @Override
            public void run() {
                swipeRefreshLayout.setRefreshing(true);
                onRefresh();
            }
        });

3、我们实现加载更多的思路是:当滚动到最后一个Item的时候,再就可以加载更多了。但是RecyclerView的布局管理器如何获取到这个情况呢?

LinearLayoutManager和GridLayoutManager的方法:
 
//最后一个可见Item的索引 int lastVisiableItemIndex = findLastVisibleItemPosition(); //总共Item的数量 int totalItemIndex = getItemCount(); //这里取<=1的时候返回true表示可以加载更多,也就是说当滑动到倒数第二个Item出现的时候就代表可以加载了


这两个布局管理器都有获取最后一个课件Item的方法,但是StaggeredGridLayoutManager 没有这个方法,用以前方法获取:

 
int[] positions = null; positions = findLastVisibleItemPositions(positions); positions[0];


因为瀑布流一行中不等高,所以从概念上来讲,就是得到一组最后出现的Item索引,我们拿到第一个进行比较就行了,不会差很多的。

 
4、瀑布流不规则,GridLayoutManager一列不占全屏幕,那么如何在最后一行显示一个充满屏幕宽度的加载更多布局?

一、GridLayoutManager的解决方案:

 

GrildLayoutManager源码对于类的解释大概有如下:

 

/**
 * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
 * <p>
 * By default, each item occupies 1 span. You can change it by providing a custom
 * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
 */

大致意思就是,每一列显示1span单位, 如果你想改变这一列所占的单位span 就调用setSpanSizeLookup方法,那么span是什么?是我们在new GridLayoutManager时传入的第二个参数就是spanCount,几列就传入几。所以重写GrildLayoutManager:

 

 

/**
 * Created by lenovo on 2016/12/20.
 */
public class MyGridLayoutManager extends GridLayoutManager implements ILayoutManager {


    public MyGridLayoutManager(BaseRecyclerAdapter adapter,Context context, int spanCount) {
        super(context, spanCount);
        setSpanSizeLookup(new BaseRecyclerAdapter.FooterSpanSize(adapter,this));
    }

    public MyGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
    }

    @Override
    public boolean loadMore() {
        //第一个可见Item的索引
        int lastVisiableItemIndex = findLastVisibleItemPosition();
        //总共Item的数量
        int totalItemIndex = getItemCount();
        //这里取<=1的时候返回true表示可以加载更多,也就是说当滑动到倒数第二个Item出现的时候就代表可以加载了
        //你也可以写成<5 或者 == 0 随便,由自己的体验来决定
        return totalItemIndex - lastVisiableItemIndex <= 1;
    }

    @Override
    public RecyclerView.LayoutManager getLayoutManager() {
        return this;
    }

    @Override
    public SpanSizeLookup getSpanSizeLookup() {
        return super.getSpanSizeLookup();
    }

}

 

在构造的时候便调用setSpanSizeLookUp设置我们自定义的lookUpSize:

 

 public  static  class  FooterSpanSize extends GridLayoutManager.SpanSizeLookup {

        private  BaseRecyclerAdapter adapter;
        private  GridLayoutManager layoutManager;

        public FooterSpanSize(BaseRecyclerAdapter adapter,GridLayoutManager layoutManager){
            this.layoutManager = layoutManager;
            this.adapter = adapter;
        }

        /**
         * 每显示一个Item的时候这个方法就会被回调,并传入item的索引position;
         * 为什么要持有Adapater的引用? 因为我们需要让Adapter来判断是否是加载更多的布局
         * 为什么要持有自身LayoutManager的引用? 因为我们需要知道总共几列
         * @param position
         * @return
         */
        @Override
        public int getSpanSize(int position) {
            if(adapter.enableLoadMore(position)){
                return layoutManager.getSpanCount();
            }
            return 1;
        }
    }


那么StaggeredGridLayoutManager是否也有这样的方法? 答案,没有,那么该如何让加载更多的Item显示整行呢?

 

 

 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        //如果是最后一个Item并且要加载更多,则只显示加载更多的布局,不进行bind数据操作
        if(position == getItemCount() - 1 && isLoadMore){
            if(holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams){
                StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
                params.setFullSpan(true);
            }
            return;
        }
        //这里其实应该写一个BaseViewHolder 调用其中的onBind抽象方法,让其具体的子类去绑定
        //不要用强转这种方式,这里为了做例子方便所以强转
        ((ContentViewHolder)holder).tvContent.setText(datas.get(position));
    }

我们只有在onBind的时候来动态判断,此viewHolder的布局的LayoutParams是不是StaggeredGridLayoutManager.LayoutParams 如果是的话就取出来,调用去setFullSpan(true),来让此Item显示一整行。有人说为什么不在ViewHolder初始化的时候就调用,我只想说。。那时候你的View还没有添加进入瀑布流的父View,自身的LayoutParams应该还不是为StaggeredGridLayoutManager.LayoutParams的吧。个人臆测,没有实际测验。

 

为什么要有ILayoutManager接口?

因为每一个layoutManager判断是否可以加载更多的逻辑不一样,所以必须用接口来编程。

 

文章中填出的代码示例直接拷贝不能立即使用,想使用请下载源码查看修改自己对应需求来使用就行。

后期我会将此Demo整理成可用项目上传至GitHub

---------------------------------------------------------------

所有的难点都已经解决,代码敲得灵活度并不是很大,不适合开源,但对应每一个想为自己项目封装一套的童鞋来说,这个完全没有问题,覆盖了所有的解决的方案。如果有什么问题,请留言。

 

源码链接-------http://download.csdn.net/detail/sinat_31311947/9717971

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值