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

开篇

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

1、在用户添加ItemDecoration的时候,会影响到刷新头部和加载更多底部的样式。

2、在用户更新列表某条记录时,不方便找到该记录对应的position。例如**notifyItemInserted(int position)**等。

效果截屏

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

gradle引用

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

属性

PullToRefreshRecyclerView

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

免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加群:936332305 / 群链接:点击链接加入群聊【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64

简析源码

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<ClassItem> 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、设置刷新(头部)

    <jsc.kit.adapter.refresh.PullToRefreshRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        app:prvHeaderLayout="@layout/xxx"
        android:id="@+id/pull_to_refresh_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

2.2、设置刷新逻辑监听

    public <H extends IHeader> 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、设置加载更多(底部)

    <jsc.kit.adapter.refresh.PullToRefreshRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        app:prvFooterLayout="@layout/xxx"
        android:id="@+id/pull_to_refresh_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

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

public <H extends IHeader> 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
                }
            };

使用介绍就到这里。

免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加群:936332305 / 群链接:点击链接加入群聊【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64

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