SwipeRefreshLayout嵌套RecyclerView实现上下拉刷新

霸气de小男生 提交于 2019-12-02 13:57:15

在这里特别感谢大神,这里附上大神帖子:https://github.com/1030310877/LoadMoreRecyclerView

SwipeRefreshLayout嵌套RecyclerView实现上下拉刷新。SwipeRedreshLayout是Android自带的一个下拉刷新控件。

它有自带的下拉刷新方法setOnRefreshListener();

//下拉刷新
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
        //最后清空数据,否则可能造成下标越界,但是业务要求先清空数据,所以,我在刷新数据的同时,将RecyclerView的滑动事件给拦截掉
        msgList.clear();

        //设置RecyclerView的滑动状态,在下拉刷新时,将RecyclerView的滑动事件给消费,连拦截
        recyclerView.setRecycleScrollBug(true);

        //加载数据
        getData("0", "20");

        //判断是否下拉刷新
        refreshFlag = 1;

        //得到刷新数据的状态
        firstFlag = 0;
    }
});

比较简单不在赘述。只是有一点需要注意:在下拉刷新的同时,如果同时做上拉加载的动作会导致下标越界。原因是,先将数据清空后,导致请求下拉无数据。但是,这是刷新,又不得不先将数据清空,所以,我在做下拉刷新的动作时,将页面滑动给禁止。我只需要禁止掉RecyclerView的滑动事件就可以了。使用如下方法。

//暂时关闭RecyclerView滑动
    public void setRecycleScrollBug(final boolean mIsRefreshing) {
        recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (mIsRefreshing) {
                    return true;
                } else {
                    return false;
                }
            }
        });
    }

很简单,只是拦截的RecyclerView的onTouch事件,return true拦截,return false不拦截。

一般来说,上拉加载更多,只需要在RecyclerView添加尾部局就可以了。但是亲测不行。

//返回item数量
    @Override
    public int getItemCount() {
        //如果返回mMsgList.size()+1会造成下标越界的异常。但是如果反回mMsgList.size()时。会造成数据少一条,加载更多试图将最后一条item给替换了
        return mMsgList.size() == 0 ? 0 : mMsgList.size() + 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (position + 1 == getItemCount()) {
            //最后一个item设置为footerView
            return TYPE_FOOTER;
        } else {
            return TYPE_ITEM;
        }
    }
如果返回mMsgList.size()+1会造成下标越界的异常。

但是如果反回mMsgList.size()时。会造成数据少一条,加载更多试图将最后一条item给替换了。

纠结了好长一段时间。因为我的思路一直停留在RecyclerView的多条目加载上面了。

这个有个弊端就是在GridLayoutManager时候就没有用了,footView或者headVeiw就跑到第一个和最后一个Grid中去了。

或者在Adapter中进行处理

这个需要通过LinearLayoutManager中findLastVisibleView的方法来判断是否到达最后。
通过GridLayoutManager中的方法设置头尾view的Grid占比来使头尾变成一整行。 这种在adapter中处理的方法,不觉得还算是自定义view的范畴,顶多是recyclerView的活用。

直到今天看到另外一种写法,直接在RecyclerView布局下面增加布局,使用使用组合View的方法,结合v4包中的NestedScrollingParent来处理和RecyclerView的滑动,然后使用NestedScrollingChild来处理嵌套在SwipeRefreshLayout中的滑动事件,达成使用SwipeRefreshLayout来进行下拉刷新。这样只要处理好滑动事件,那就不管是什么LayoutManager,footView永远在最下面。

上代码

public class MyRecyclerView extends LinearLayout implements NestedScrollingParent, NestedScrollingChild {
    private View rootLayout;
    private RecyclerView recyclerView;
    private FrameLayout footView;
    private NestedScrollingParentHelper helper;
    private NestedScrollingChildHelper childHelper;
    private boolean isBottom = false;
    private boolean changeBottom = false;
    private boolean enableLoad = true;
    private boolean isLoading = false;


    //TODO footView的内容View
    private View footContentView;

    private final int[] mScrollOffset = new int[2];
    private ProgressBar progressBar;

    public MyRecyclerView(Context context) {
        this(context, null);
    }

    public MyRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        helper = new NestedScrollingParentHelper(this);
        childHelper = new NestedScrollingChildHelper(this);
        childHelper.setNestedScrollingEnabled(true);
        initView();
    }

    private void initView() {
        View.inflate(getContext(), R.layout.layout_myrecycler, this);
        recyclerView = (RecyclerView) findViewById(R.id.recycler_content);
        footView = (FrameLayout) findViewById(R.id.layout_footView);
        //TODO
        footContentView = LayoutInflater.from(getContext()).inflate(R.layout.footview_up, null);
        footView.addView(footContentView);
        rootLayout = getChildAt(0);
    }

    private void smoothScrollBy(int dx, int dy) {
        //将ProgressBar隐藏
        findViewById(R.id.item_load_pb).setVisibility(GONE);
        //设置mScroller的滚动偏移量
        if (isBottom) {
            //已经到了底部,并且还在往上拉,直接返回,不处理事件
            if (rootLayout.getScrollY() + dy >= footView.getMeasuredHeight()) {
                return;
            }
            //回弹
            if (rootLayout.getScrollY() + dy <= 0) {
                dy = -rootLayout.getScrollY();
            }
            changeBottom = false;
            rootLayout.scrollBy(0, dy);
            invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
        } else {
            if (!isLoading) {
                //TODO 重置视图
                ((TextView) footContentView.findViewById(R.id.status_tv)).setText("上拉加载");
            }
            //往上拉时,距离大于了footview的高度,只让它拉到那么大
            if (rootLayout.getScrollY() + dy >= footView.getMeasuredHeight()) {
                dy = footView.getMeasuredHeight() - rootLayout.getScrollY();
                changeBottom = true;
                //TODO 拉到footView最大高度时候可以做的事情,视图变化
                if (!isLoading) {
                    ((TextView) footContentView.findViewById(R.id.status_tv)).setText("松开加载更多");
                }
            } else {
                //往下拉时,滑动距离大于了当前的偏移值
                if (rootLayout.getScrollY() + dy < 0) {
                    dy = -rootLayout.getScrollY();
                }
            }
            rootLayout.scrollBy(0, dy);
            invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
        }
    }

    /*==========以下为Child的方法==========*/

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        childHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return childHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return childHelper.startNestedScroll(axes);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public void stopNestedScroll() {
        childHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return childHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return childHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    /*==========以下为Parent的方法,用来接收RecyclerView的滑动事件==========*/
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        startNestedScroll(nestedScrollAxes);
        return enableLoad;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        helper.onNestedScrollAccepted(child, target, axes);
    }

    @Override
    public int getNestedScrollAxes() {
        return helper.getNestedScrollAxes();
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy < 0 && rootLayout.getScrollY() > 0) {
            //如果是手指下滑
            smoothScrollBy(dx, dy);
            consumed[1] = dy;
        } else {
            dispatchNestedPreScroll(dx, dy, consumed, mScrollOffset);
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if (dyConsumed == 0 && dyUnconsumed > 0) {
            smoothScrollBy(dxUnconsumed, dyUnconsumed);
        } else {
            //其余的未消费的事件,传给父View消费
            dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mScrollOffset);
        }
    }

    @Override
    public void onStopNestedScroll(View child) {
        //将ProgressBar展示
        findViewById(R.id.item_load_pb).setVisibility(VISIBLE);
        isBottom = changeBottom;
        if (isBottom && !isLoading && rootLayout.getScrollY() == footView.getMeasuredHeight()) {
            isLoading = true;
            //TODO LoadMore视图变化
            ((TextView) footContentView.findViewById(R.id.status_tv)).setText("正在加载");
            if (listener != null) {
                listener.onLoading();
            }
        } else {
            isBottom = false;
            if (rootLayout.getScrollY() > 0 && !isBottom) {
                smoothScrollBy(0, -rootLayout.getScrollY());
            }
        }
        helper.onStopNestedScroll(child);
        //最后一定要调用这个,告诉父view滑动结束,不然父view的滑动会卡住。
        stopNestedScroll();
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
        return false;
    }

    /*==============以下为开放部分的recyclerView方法 ================*/
    //TODO  如果还需要其他recyclerview的方法,可在下方进行开放。

    public void setAdapter(RecyclerView.Adapter adapter) {
        recyclerView.setAdapter(adapter);
    }

    public void setLayoutManager(RecyclerView.LayoutManager layoutManager) {
        recyclerView.setLayoutManager(layoutManager);
    }

    public void addItemDecoration(RecyclerView.ItemDecoration decoration) {
        recyclerView.addItemDecoration(decoration);
    }

    public void addItemDecoration(RecyclerView.ItemDecoration decoration, int index) {
        recyclerView.addItemDecoration(decoration, index);
    }

    public RecyclerView.Adapter getAdapter() {
        return recyclerView.getAdapter();
    }

    public RecyclerView.LayoutManager getLayoutManager() {
        return recyclerView.getLayoutManager();
    }

    public void addOnScrollListener(RecyclerView.OnScrollListener onScrollListener) {
        recyclerView.addOnScrollListener(onScrollListener);
    }

    /**
     * 加载结束后调用该方法进行footview缩回
     *
     * @param total
     * @param size
     */
    public void loadFinished(int total, int size) {
        isLoading = false;

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (isBottom) {
                    smoothScrollBy(0, -footView.getMeasuredHeight());
                }
            }
        }, 200);
        if (total == size){
            //TODO 加载完成后的处理,缩回以及视图变化
            //将ProgressBar隐藏
            findViewById(R.id.item_load_pb).setVisibility(GONE);
            ((TextView) footContentView.findViewById(R.id.status_tv)).setText("无更多数据");
        }else {
            //TODO 加载完成后的处理,缩回以及视图变化
            ((TextView) footContentView.findViewById(R.id.status_tv)).setText("加载成功");
        }
    }

    public void setEnableLoad(boolean tf) {
        this.enableLoad = tf;
    }

    private Handler handler = new Handler();
    /*=============== 监听 ===================*/
    public onLoadingMoreListener listener;

    public void setOnLoadingListener(onLoadingMoreListener listener) {
        this.listener = listener;
    }

    public interface onLoadingMoreListener {
        void onLoading();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * 关闭RecyclerView滑动
     * @param mIsRefreshing  true表示拦截滑动事件,页面不可滑动,false表示不拦截事件,页面可以滑动
     */
    public void setRecycleScrollBug(final boolean mIsRefreshing) {
        recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (mIsRefreshing) {
                    return true;
                } else {
                    return false;
                }
            }
        });
    }
}

贴上布局代码:layout_myrecycler

<?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="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <FrameLayout
        android:id="@+id/layout_footView"
        android:layout_width="match_parent"
        android:layout_height="50dp" />
</LinearLayout>

布局代码:footview_up

<?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="80dp"
    android:gravity="center"
    android:orientation="horizontal">
    <ProgressBar
        android:id="@+id/item_load_pb"
        style="@android:style/Widget.Holo.ProgressBar"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_centerHorizontal="true"
        />
    <TextView
        android:id="@+id/status_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/ten_dp"
        android:text="上拉刷新" />
</LinearLayout>

不过这样还是会出现下拉刷新时偶尔会造成下标越界的Bug,原因还是一样,刷新的同时,将数据清空导致的。我在这里使用了一个方法,再刷新时,写一个标识,在请求数据时,得到数据之前,将之前存在的数据清空,这样就不会导致下标越界了

/**
         * 如果绑定的 List 对象在更新数据之前进行了 clear,而这时用户紧接着迅速上滑RecycleView,就会造成崩溃,而且异常不会报到你的代码上,属于RecycleView的内部错误。
         * 原因是当你 clear 了 list 之后,这时迅速上滑,而新数据还没到来,导致 RecycleView 要更新加载下面的 Item 时候,找不到数据源了,造成程序直接崩溃了
         * 思路:当你在下拉刷新时,将RecyclerView设为不可滑动状态,刷新完成在设置为可滑动状态。暂时解决
         */
        //下拉刷新
        swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                //设置RecyclerView的滑动状态,在下拉刷新时,将RecyclerView的滑动事件给消费,连拦截
                recyclerView.setRecycleScrollBug(true);
                try {
                    //最后清空数据,否则可能造成下标越界,但是业务要求先清空数据,所以,我在刷新数据的同时,将RecyclerView的滑动事件给拦截掉
                    //msgList.clear();
                    //设置标识位,在刷新时,得到数据前,将数据清空,而不是在刷新方法里清空
                    pageindex = 1;
                    getData("0", "" + NEXNUM);
                    //判断是否下拉刷新
                    refreshFlag = 1;
                    //得到刷新数据的状态
                    totalFlag = 0;
                } catch (Exception e) {
                    e.printStackTrace();
                    ToastUtils.showShort("操作次数过于频繁,请稍后再试");
                }
            }
        });

源码在文章开头的github里面能找到。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!