Android 支持刷新、加载更多、带反弹效果的RecyclerView

开篇

  当前市面上很多支持刷新、加载更多RecyclerView开源库,为何我这里还要自己再写一个?因为市面上的这些支持刷新加载更多的RecyclerView开源库实现方式基本上都是:在Adapter的外层在包裹一层Adapter,这种实现方式主要有以下两个不方便

  • 1、在用户添加ItemDecoration的时候,会影响到刷新头部和加载更多底部的样式。
  • 2、在用户更新列表某条记录时,不方便找到该记录对应的position。例如notifyItemInserted(int position)等。

效果截屏

Android 支持刷新、加载更多、带反弹效果的RecyclerView_第1张图片
SimpleAdapter.png
Android 支持刷新、加载更多、带反弹效果的RecyclerView_第2张图片
PullToRefreshRecyclerView

立即体验

扫描以下二维码下载体验App(体验App内嵌版本更新检测功能):


Android 支持刷新、加载更多、带反弹效果的RecyclerView_第3张图片
扫描下载体验App

传送门:https://github.com/JustinRoom/SimpleAdapterDemo

gradle引用

    implementation 'jsc.kit.adapter:adapter-component:_latestVersion'

属性

PullToRefreshRecyclerView

名称 类型 描述
prvHeaderLayout reference 下拉刷新头部layout
prvFooterLayout reference 上拉加载更多底部layout
prvPullDownToRefreshText string 下拉刷新提示
prvReleaseToRefreshText string 释放刷新提示
prvRefreshingText string 正在刷新提示
prvRefreshCompletedText string 刷新完成提示
prvPullUpToLoadMoreText string 上拉加载更多提示
prvReleaseToLoadMoreText string 释放加载更多提示
prvLoadingMoreText string 正在加载更多提示
prvLoadMoreCompletedText string 加载更多完成提示

简析源码

public class PullToRefreshRecyclerView extends ViewGroup {}

1、初始化布局

    private void initView(Context context) {
        inflate(context, R.layout.recycler_pull_to_refresh_recycler_view, this);
        recyclerView = findViewById(R.id.recycler_view);

        final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        scaledTouchSlop = viewConfiguration.getScaledTouchSlop();
    }

    private void initAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefreshRecyclerView, defStyleAttr, 0);
        int headerLayoutId = a.getResourceId(R.styleable.PullToRefreshRecyclerView_prvHeaderLayout, -1);
        int footerLayoutId = a.getResourceId(R.styleable.PullToRefreshRecyclerView_prvFooterLayout, -1);

        //refresh text
        pullDownToRefreshText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvPullDownToRefreshText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvPullDownToRefreshText) :
                getResources().getString(R.string.recycler_default_pull_down_to_refresh);
        releaseToRefreshText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvReleaseToRefreshText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvReleaseToRefreshText) :
                getResources().getString(R.string.recycler_default_release_to_refresh);
        refreshingText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvRefreshingText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvRefreshingText) :
                getResources().getString(R.string.recycler_default_refreshing);
        refreshCompletedText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvRefreshCompletedText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvRefreshCompletedText) :
                getResources().getString(R.string.recycler_default_refresh_completed);

        //load more text
        pullUpToLoadMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvPullUpToLoadMoreText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvPullUpToLoadMoreText) :
                getResources().getString(R.string.recycler_default_pull_up_to_load_more);
        releaseToLoadMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvReleaseToLoadMoreText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvReleaseToLoadMoreText) :
                getResources().getString(R.string.recycler_default_release_to_load_more);
        loadingMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvLoadingMoreText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvLoadingMoreText) :
                getResources().getString(R.string.recycler_default_loading_more);
        loadMoreCompletedText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvLoadMoreCompletedText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvLoadMoreCompletedText) :
                getResources().getString(R.string.recycler_default_load_more_completed);
        a.recycle();

        if (headerLayoutId == -1) {
            headerView = LayoutInflater.from(context).inflate(R.layout.recycler_default_header_view, this, false);
            setHeader(createDefaultHeader());
        } else {
            headerView = LayoutInflater.from(context).inflate(headerLayoutId, this, false);
        }
        if (footerLayoutId == -1) {
            footerView = LayoutInflater.from(context).inflate(R.layout.recycler_default_footer_view, this, false);
            setFooter(createDefaultFooter());
        } else {
            footerView = LayoutInflater.from(context).inflate(footerLayoutId, this, false);
        }
        addView(headerView, 0);
        addView(footerView);


        setHaveMore(false);
    }

2、测量

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        headerHeight = headerView.getMeasuredHeight();
        footerHeight = footerView.getMeasuredHeight();
    }

3、排版页面元素

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        headerView.layout(0, 0 - headerView.getMeasuredHeight(), getMeasuredWidth(), 0);
        recyclerView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
        footerView.layout(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight() + footerView.getMeasuredHeight());
    }

4、touch事件分发拦截。这里我们只拦截滑动事件,其他事件交由RecyclerView自己去处理。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (getState() == REFRESH_COMPLETED
                || getState() == LOAD_MORE_COMPLETED)
            return super.onInterceptTouchEvent(ev);

        int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                stopReboundAnim();
                recyclerView.stopScroll();
                lastTouchY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float curTouchY = ev.getY();
                float dy = curTouchY - lastTouchY;
                dy = dy > 0 ? dy + 0.5f : dy - 0.5f;
                lastTouchY = curTouchY;
                //如果滑动距离小于scaledTouchSlop,则把事件交给子View消耗;
                //否则此事件交由自己的onTouchEvent(MotionEvent event)方法消耗。
                if (Math.abs((int) dy) >= scaledTouchSlop / 2)
                    return true;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

5、处理拦截到的滑动事件。VelocityTracker跟踪滑动速度。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        enSureVelocityTrackerNonNull();
        int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                trackMotionEvent(ev);
                lastTouchY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                trackMotionEvent(ev);
                float curTouchY = ev.getY();
                float dy = curTouchY - lastTouchY;
                if (dy != 0) {
                    dy = dy > 0 ? dy + 0.5f : dy - 0.5f;
                    lastTouchY = curTouchY;
                    executeMove((int) -dy);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                final VelocityTracker tracker = velocityTracker;
                tracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocity = (int) tracker.getYVelocity();
                recycleVelocityTracker();
                executeUpOrCancelMotionEvent(velocity);
                break;
        }
        return true;
    }

6、执行滑动。

    private void executeMove(int distance) {
        if (distance == 0)
            return;

        int scrollY = getScrollY();
        int scrolledY = 0;
        if (distance < 0) {//向下滑动
            //如果正在加载更多,我们避免加载更多底部视图被滑动至不可见。
            if (!isLoadingMore() && scrollY > 0) {
                scrolledY = Math.max(0 - scrollY, distance);
                scrollBy(0, scrolledY);
                distance = distance - scrolledY;
            }

            //滑动列表。
            scrolledY = Math.max(0 - getRecyclerViewMaxCanPullDownDistance(), distance);
            if (scrolledY != 0)
                recyclerView.scrollBy(0, scrolledY);

            //如果正在加载更多且已滑动至列表顶部,不可再向下滑动。
            if (!isLoadingMore()) {
                distance = distance - scrolledY;
                distance = toScaledValue(distance);
                if (distance != 0)
                    scrollBy(0, distance);
            }
        } else {//向上滑动
            //如果正在刷新,我们避免刷新头部视图别滑动至不可见。
            if (!isRefreshing() && scrollY < 0) {
                scrolledY = Math.min(Math.abs(scrollY), distance);
                scrollBy(0, scrolledY);
                distance = distance - scrolledY;
            }

            //滑动列表
            scrolledY = Math.min(getRecyclerViewMaxCanPullUpDistance(), distance);
            if (scrolledY != 0)
                recyclerView.scrollBy(0, scrolledY);

            //如果正在刷新且已滑动至列表底部,不可再向上滑动。
            if (!isRefreshing()) {
                distance = distance - scrolledY;
                distance = toScaledValue(distance);
                if (distance != 0)
                    scrollBy(0, distance);
            }
        }

        if (getScrollY() < 0) {
            if (!isRefreshEnable() || isRefreshing()) {
                header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
                return;
            }

            // getRefreshThresholdValue()释放执行刷新阈值
            if (getScrollY() < getRefreshThresholdValue()) {
                //release to refresh
                setState(RELEASE_TO_REFRESH);
            } else {
                //pull down to refresh
                setState(PULL_DOWN_TO_REFRESH);
            }
            header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
        } else if (getScrollY() > 0) {
            if (!isLoadMoreEnable() || isLoadingMore()) {
                footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
                return;
            }

            // getLoadMoreThresholdValue()释放执行加载更多阈值
            if (getScrollY() > getLoadMoreThresholdValue()) {
                //release to load more
                setState(RELEASE_TO_LOAD_MORE);
            } else {
                //pull up to load more
                setState(PULL_UP_TO_LOAD_MORE);
            }
            footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
        } else {
            header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
            footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
        }
    }

7、执行touch结束事件。

    private void executeUpOrCancelMotionEvent(int velocity) {
        switch (getState()) {
            case REFRESHING:
                executeRebound(0 - headerHeight);
                recyclerView.fling(0, 0 - velocity);
                break;
            case LOADING_MORE:
                executeRebound(footerHeight);
                recyclerView.fling(0, 0 - velocity);
                break;
            case RELEASE_TO_REFRESH:
                executeRebound(0 - headerHeight);
                break;
            case RELEASE_TO_LOAD_MORE:
                executeRebound(isHaveMore() ? footerHeight : 0);
                break;
            default:
                executeRebound(0);
                recyclerView.fling(0, 0 - velocity);
                break;
        }
    }

    private void executeRebound(int destinationScrollY) {
        int scrollYDistance = destinationScrollY - getScrollY();
        int duration = Math.abs(scrollYDistance);
        duration = Math.max(200, duration);
        duration = Math.min(500, duration);
        if (animator == null) {
            animator = ObjectAnimator.ofPropertyValuesHolder(this, PropertyValuesHolder.ofInt(SCROLL_Y, getScrollY(), destinationScrollY));
            animator.setInterpolator(new AccelerateDecelerateInterpolator());
            animator.addListener(new SimpleAnimatorListener() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    switch (getState()) {
                        case RELEASE_TO_REFRESH:
                            if (!isRefreshing() && onRefreshListener != null) {
                                setState(REFRESHING);
                                currentPage = startPage;
                                onRefreshListener.onRefresh(getContext(), currentPage, pageSize);
                            }
                            break;
                        case RELEASE_TO_LOAD_MORE:
                            if (isHaveMore() && !isLoadingMore() && onRefreshListener != null) {
                                setState(LOADING_MORE);
                                currentPage++;
                                onRefreshListener.onLoadMore(getContext(), currentPage, pageSize);
                            }
                            break;
                        case REFRESH_COMPLETED:
                            setState(INIT);
                            lastRefreshTimeStamp = System.currentTimeMillis();
                            header.updateLastRefreshTime(lastRefreshTimeStamp);
                            break;
                        case LOAD_MORE_COMPLETED:
                            setState(INIT);
                            break;
                    }
                }
            });
        } else {
            animator.setIntValues(getScrollY(), destinationScrollY);
        }
        animator.setDuration(duration);
        animator.start();
    }

使用示例

  • 1、简单使用示例:
        PullToRefreshRecyclerView pullToRefreshRecyclerView;
    
        //设置分页加载的起始页序号以及每页数据数量
        pullToRefreshRecyclerView.initializeParameters(1, 10);
        //关闭下拉刷新
//        pullToRefreshRecyclerView.setRefreshEnable(false);
        //关闭加载更多
//        pullToRefreshRecyclerView.setLoadMoreEnable(false);
        //设置下拉刷新和上拉加载更多监听
        pullToRefreshRecyclerView.setOnRefreshListener(new PullToRefreshRecyclerView.OnRefreshListener() {
            @Override
            public void onRefresh(@NonNull Context context, int currentPage, int pageSize) {
                index = -1;
                loadNetData();
            }

            @Override
            public void onLoadMore(@NonNull Context context, int currentPage, int pageSize) {
                loadNetData();
            }
        });
        RecyclerView recyclerView = pullToRefreshRecyclerView.getRecyclerView();
        recyclerView.setLayoutManager(new LinearLayoutManager(inflater.getContext()));
        recyclerView.addItemDecoration(new SpaceItemDecoration(
                CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16),
                CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_2)
        ));
        
        
//模拟加载网络数据
    private int index = -1;
    private Random random = new Random();
    private void loadNetData(){
        pullToRefreshRecyclerView.postDelayed(new Runnable() {
            @Override
            public void run() {
                //刷新(或加载更多)完成
                pullToRefreshRecyclerView.completed();
                List items = new ArrayList<>();
                int count = 7 + random.nextInt(12);
                for (int i = 0; i < count; i++) {
                    index ++;
                    ClassItem item = new ClassItem();
                    item.setLabel("this is " + index);
                    items.add(item);
                }

                //判定是否是第一页数据
                if (pullToRefreshRecyclerView.isFirstPage()) {
                    adapter3.setData(items);
                } else {
                    adapter3.addData(items);
                }
                //设置是否还有下一页数据
                pullToRefreshRecyclerView.setHaveMore(items.size() >= pullToRefreshRecyclerView.getPageSize());
            }
        }, 50 + random.nextInt(2000));
    }
  • 2、自定义下拉刷新:

2.1、设置刷新(头部)


2.2、设置刷新逻辑监听

public  void setHeader(@NonNull H header)

2.3、实现刷新逻辑

        IHeader header =  new IHeader() {

            @Override
            public void initChildren(@NonNull View headerView) {
                //这里初始化下拉刷新view
                //也就是app:prvHeaderLayout="@layout/xxx"属性对应的布局文件
            }

            @Override
            public void updateLastRefreshTime(long lastRefreshTimeStamp) {
                //这里是上次刷新时间更新监听
            }

            @Override
            public void onUpdateState(int state, CharSequence txt) {
                //这里是监听下拉刷新的各种状态
                //监听到的状态有:PULL_DOWN_TO_REFRESH、RELEASE_TO_REFRESH、REFRESHING、REFRESH_COMPLETED
                switch (state) {
                    case PullToRefreshRecyclerView.REFRESHING:
                        //正在刷新,我们可以正在这里启动正在刷新的动画
                        
                        break;
                    case PullToRefreshRecyclerView.REFRESH_COMPLETED:
                        //刷新完成,我们可以在这里关闭正在刷新的动画以及头部复位
                        
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onScroll(int state, boolean refreshEnable, boolean isRefreshing, int scrollY, int headerHeight, int refreshThresholdValue) {
                //这里是监听下拉刷新动作
                //监听到的状态有:INIT、PULL_DOWN_TO_REFRESH、RELEASE_TO_REFRESH、REFRESHING、REFRESH_COMPLETED
            }
        };
  • 3、自定义上拉加载更多:

3.1、设置加载更多(底部)


3.2、设置加载更多逻辑监听

public  void setHeader(@NonNull H header)

3.3、实现加载更多逻辑

        IFooter footer = new IFooter() {

            @Override
            public void initChildren(@NonNull View footerView) {
                //这里初始化上拉加载更多view
                //也就是app:prvFooterLayout="@layout/xxx"属性对应的布局文件
            }

            @Override
            public void onUpdateState(@State int state, CharSequence txt) {
                //这里是监听上拉加载更多的各种状态
                //监听到的状态有:PULL_UP_TO_LOAD_MORE、RELEASE_TO_LOAD_MORE、LOADING_MORE、LOAD_MORE_COMPLETED
                switch (state) {
                    case PullToRefreshRecyclerView.LOADING_MORE:
                        //正在加载更多,我们可以正在这里启动正在加载更多的动画
                        
                        break;
                    case PullToRefreshRecyclerView.LOAD_MORE_COMPLETED:
                        //加载更多完成,我们可以在这里关闭正在加载更多的动画以及底部复位
                        
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onScroll(int state, boolean loadMoreEnable, boolean isLoadingMore, int scrollY, int footerHeight) {
                //这里是监听上拉加载更多动作
                //监听到的状态有:INIT、PULL_UP_TO_LOAD_MORE、RELEASE_TO_LOAD_MORE、LOADING_MORE、LOAD_MORE_COMPLETED
            }
        };

使用介绍就到这里。
从0撸出这个开源库不容易,希望童鞋们在GitHub上给一颗星星✨支持下。谢谢!如果在使用过程中不懂(或需要改进的地方),可以在评论里给我留言,也可以联系我。
微信:eoy9527QQ:1006368252

篇尾

在人生的道路上,当你的希望一个个落空的时候,你也要坚定,要沉着。 —— 朗费罗

你可能感兴趣的:(Android 支持刷新、加载更多、带反弹效果的RecyclerView)