美文网首页Android 进阶技术篇专题我爱编程Android开发
Android 支持刷新、加载更多、带反弹效果的Recycler

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

作者: SwitchLife | 来源:发表于2019-01-05 19:43 被阅读10次

    开篇

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

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

    效果截屏

    PullToRefreshRecyclerView

    立即体验

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


    扫描下载体验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<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
                }
            };
    

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

    篇尾

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

    相关文章

      网友评论

        本文标题:Android 支持刷新、加载更多、带反弹效果的Recycler

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