Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理

RecyclerView已经写过两篇文章了,分别是Android 5.X新特性之RecyclerView基本解析及无限复用Android 5.X新特性之为RecyclerView添加HeaderView和FooterView,既然来到这里还没学习的,先去学习下吧。

今天我们的主题是学习为RecyclerView添加下拉刷新和上拉加载功能。

首先,我们先来学习下拉刷新,google公司已经为我们提供的一个很好的包装类,那就是SwipeRefreshLayout,这个类可以支持我们向下滑动并进行监听。那么我们先了解一些基本知识,然后再从源码的角度来解析它。

A. SwipeRefreshLayout 是一个容器,直接继承于ViewGroup。

从其源码中我们可以直接看出,它是直接继承于ViewGroup的,所以它是一个容器,既然是一个容器,那么我们就可以向其中添加View。

B. SwipeRefreshLayout 封装了一些列的方法供我们使用,其中较常用的包括以下几个。

1. setColorSchemeResources: 刷新时动画的颜色,可以设置4个
2. setProgressBackgroundColorSchemeResource: 设置刷新时进度圆环的背景颜色
3. setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener): 设置手势滑动监听器。
4. setRefreshing(Boolean refreshing): 设置组件的刷洗状态。
5. setSize(int size):设置进度圈的大小,只有两个值:DEFAULT、LARGE

其中最主要的是setOnRefreshListener,它是用来监听我们下拉手势的回调方法。

C. 接下来我们再从源码的角度来了解这个类:

SwipeRefreshLayout 是一个ViewGroup容器,那在向它添加子View的时候,那首先会去测量各个子View的大小来确定本身的大小,并且还会制定子View的坐标位置,最后绘制View并显示出来。针对ViewGroup的绘制我之前有写过一篇博文,大家可以去参考下Android自定义控件之继承ViewGroup创建新容器(四) ,里面有详细的讲解。而我们今天所要讲解的是从SwipeRefreshLayout 的事件机制来说起,也更符合我们下拉刷新的主题。

在SwipeRefreshLayout 的事件拦截分发器onInterceptTouchEvent中,它是这么定制的,源码如下:

	@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();

        final int action = MotionEventCompat.getActionMasked(ev);

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                final float initialDownY = getMotionEventY(ev, mActivePointerId);
                if (initialDownY == -1) {
                    return false;
                }
                mInitialDownY = initialDownY;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                    return false;
                }

                final float y = getMotionEventY(ev, mActivePointerId);
                if (y == -1) {
                    return false;
                }
                final float yDiff = y - mInitialDownY;
                if (yDiff > mTouchSlop && !mIsBeingDragged) {
                    mInitialMotionY = mInitialDownY + mTouchSlop;
                    mIsBeingDragged = true;
                    mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
                }
                break;

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }

它最终返回的是代表是否滑动的mIsBeingDragged布尔值。在我们按下,抬起,或取消时mIsBeingDragged的值是false,意思是在这几个动作中,SwipeRefreshLayout 本身是不拦截事件的,而是传递给父类,让父类进行处理。而我们主要来看MotionEvent.ACTION_MOVE:这个动作,它首先判断是否是可用的活动id: mActivePointerId,然后根据得到mActivePointerId来获取滑动的中坐标距离值:Y,然后做出判断:如果Y==-1就代表没滑动,所以直接返回false表示不拦截;如果Y值大于规定的最小滑动距离mTouchSlop值,并且!mIsBeingDragged为真,那么就让mIsBeingDragged == true;并返回,也就是在这种情况下,SwipeRefreshLayout 它自己消化了事件,而不是传递给父类。因此,当我们在向下滑动了一定的距离时,SwipeRefreshLayout 就是捕捉到当前的事件。

那么我们再来看看它是怎么处理当前捕捉到的事件的。请看源码:

	@Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex = -1;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                break;

            case MotionEvent.ACTION_MOVE: {
                pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }

                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                if (mIsBeingDragged) {
                    if (overscrollTop > 0) {
                        moveSpinner(overscrollTop);
                    } else {
                        return false;
                    }
                }
                break;
            }
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                pointerIndex = MotionEventCompat.getActionIndex(ev);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                    return false;
                }
                mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                break;
            }

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP: {
                pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    return false;
                }

                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                mIsBeingDragged = false;
                finishSpinner(overscrollTop);
                mActivePointerId = INVALID_POINTER;
                return false;
            }
            case MotionEvent.ACTION_CANCEL:
                return false;
        }

        return true;
    }

同样的道理在MotionEvent.ACTION_DOWN和case MotionEvent.ACTION_CANCEL时不处理事件,交给父类处理。而在MotionEvent.ACTION_MOVE:中获取到与顶端窗口的overscrollTop,如果overscrollTop值大于0就调用moveSpinner(overscrollTop);方法来初始化mCircleView旋转的。最后在MotionEvent.ACTION_UP:抬起事件中,同样获取overscrollTop,且调用finishSpinner(overscrollTop);方法来完成mCircleView的旋转事件并回复一些属性配置值。

然后我们再来看看finishSpinner(overscrollTop);方法中是怎么处理的。

private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {
            setRefreshing(true, true /* notify */);
        } else {
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            Animation.AnimationListener listener = null;
            if (!mScale) {
                listener = new Animation.AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (!mScale) {
                            startScaleDownAnimation(null);
                        }
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }

                };
            }
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        }
    }

方法里面很简单,if (overscrollTop > mTotalDragDistance) 就调用setRefreshing(true, true /* notify */);用来设置刷新事件的,否则就回复初始前的属性配置值。

再来看看setRefreshing(true, true)方法:

private void setRefreshing(boolean refreshing, final boolean notify) {
        if (mRefreshing != refreshing) {
            mNotify = notify;
            ensureTarget();
            mRefreshing = refreshing;
            if (mRefreshing) {
                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
            } else {
                startScaleDownAnimation(mRefreshListener);
            }
        }
    }

也很好理解,因为传进来的refreshing值为true,所以它会调用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);来开启mCircleView的动画展示,并传进了mRefreshListener监听器,这个监听器是什么呢?来看看

private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }

        @Override
        public void onAnimationEnd(Animation animation) {
            if (mRefreshing) {
                // Make sure the progress view is fully visible
                mProgress.setAlpha(MAX_ALPHA);
                mProgress.start();
                if (mNotify) {
                    if (mListener != null) {
                        mListener.onRefresh();
                    }
                }
                mCurrentTargetOffsetTop = mCircleView.getTop();
            } else {
                reset();
            }
        }
    };

它是一个动画监听器,在动画结束时调用mListener.onRefresh();而mListener是一个接口,里面封装了一个onRefresh()的方法,并且它暴露了对外调用的方法setOnRefreshListener(),所以我们可以在Activity中调用该方法可以实现我们自己的逻辑业务。

ok,到这里,相信大家都知道了wipeRefreshLayout.setOnRefreshListener();的工作原理,那么我们现在来实现我们的刷新功能吧;

首先,我们的布局文件先把RecyclerView放到SwipeRefreshLayout容器中:

recycer_view.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srl_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            custom:listDividerSize="2dp"
            custom:listDividerBackgroundColor="#FF0000"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </android.support.v7.widget.RecyclerView>
    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

然后RecycerActivity中配置一些SwipeRefreshLayout属性值,并调用setOnRefreshListener方法并在onRefresh()实现自己的逻辑业务:

srl_refresh.setColorSchemeResources(android.R.color.holo_blue_light,
                android.R.color.holo_red_light,android.R.color.holo_orange_light,
                android.R.color.holo_green_light);
        srl_refresh.setProgressBackgroundColorSchemeResource(android.R.color.white);
        srl_refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        List<String> newDatas = new ArrayList<String>();
                        for (int i = 0; i <5; i++) {
                            int index = i + 1;
                            newDatas.add("new item" + index);
                        }
                        mBaseRecyclerAdapter.addDatas(newDatas);
                        srl_refresh.setRefreshing(false);
                        Toast.makeText(RecycerActivity.this, "更新了五条数据...", Toast.LENGTH_SHORT).show();
                    }
                }, 5000);
            }
        });

来看看结果吧
这里写图片描述

好了,RecyclerView利用SwipeRefreshLayout实现上拉刷新我们已经实现了,并且也带大家看过它的实现原理了,相信大家一定能更好的掌握它了,那么接下来我们就来实现上拉加载了。

在上一讲中,我们已经实现了在底部添加上了一个FooterView,那么我们现在可以利用它来实现我们的上拉加载。
其思想我们可以这样设计,当我们滑动到最后一个ItemView时,让它去加载数据,那怎么获取到列表的最后一个ItemView呢?所幸的是,在RecyclerView中封装的LayoutManger子类中有这样的方法可以供我们获取到最后一个ItemView,该方法是findLastVisibleItemPosition();那我们又该怎么监听RecyclerView滑动呢?可以调用它的addOnScrollListener()方法,由此我们找到了解决方案

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if(newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount()){
                    mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADING_MORE);
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            List<String> newDatas = new ArrayList<String>();
                            for (int i = 0; i< 5; i++) {
                                int index = i +1;
                                newDatas.add("more item" + index);
                            }
                            if(newDatas == null){
                                mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADED_MORE);
                                return;
                            }
                            mBaseRecyclerAdapter.addMoreDatas(newDatas);
                            mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOAD_MORE);
                            Toast.makeText(RecycerActivity.this,"已加载了数据", Toast.LENGTH_SHORT).show();
                        }
                    },1000);
                }
            }
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition();
            }
        });

代码解释:首先我们会在onScrolled方法中回去到最后一行的ItenView,然后再onScrollStateChanged方法中进行必要的判断,如果lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount(),那么就可以确定给ItemView是最后一个ItemView,然后就可以用来实现我们的业务逻辑了,在这里我让它新加了5条数据,然后更新Adapter。

最后在onBindViewHolder稍作修改,如下

 @Override
    public void onBindViewHolder(BaseViewHolderHelper holder, int position) {
        //把每一个itemView设置一个标签,方便以后根据标签获取到该itemView以便做其他事项,比较点击事件
        if(getItemViewType(position) == TYPE_HEADER){
            return;
        }else if(getItemViewType(position) == TYPE_FOOTER){
            FooterViewHolder footViewHolder=(FooterViewHolder)holder;
            footViewHolder.footView.setText("上拉加载更多...");
            switch (status){
                case LOAD_MORE:
                    footViewHolder.footView.setText("上拉加载更多...");
                    break;
                case LOADING_MORE:
                    footViewHolder.footView.setText("正在加载中...");
                    break;
                case LOADED_MORE:
                    footViewHolder.footView.setText("已加载完毕");
                    break;
            }
        } else{
           ...
        }
    }

ok,来看看结果吧:
这里写图片描述

好了,已经实现了上拉加载的功能了,相信大家也都可以做很多事情了。

总结:本节主题是为RecyclerView添加下拉刷新和上拉加载的功能,基本的思路也都已讲清楚了,而且着重的讲解了一下利用SwipeRefreshLayout实现下拉刷新的实现原理,相信大家通过这节更能学到一些原理性的东西,ok,今天就讲到这里吧。祝大家学习愉快。

更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。

这里写图片描述

posted @ 2016-11-25 08:57  管满满  阅读(2698)  评论(0编辑  收藏  举报