PullToRefreshRecyclerView封装实现

原生RecyclerView无法支持下拉刷新及上拉加载等操作,需要封装才能支持。考虑到不仅仅是RecyclerView可能需要该操作,任何一个View都有可能需要,因此将上下拉设计为一个可容纳三个子View的容器(headerView,innerView和footerView)。

PullToRefreshRecyclerView总体思路

NetableView

封装了三个状态view(Loading、Empty、Error)并从外部传入一个innerView(可以是任意View,作为内容显示的view)。可通过setNetState(int state)控制状态页面的展示。状态类型如下:

  • DATA_STATUS_LOADING = -1;
  • DATA_STATUS_EMPTY = 0;
  • DATA_STATUS_NORMAL = 1;
  • DATA_STATUS_ERROR = 2;

NetableRecyclerView

组合了RecyclerView及NetStateView,并将RecyclerView传入NetStateView以进行状态统一管控。通过提供的notifyNetState(int state)可直接更新页面数据状态。setDefaultRetryClickListener()可设置默认Error页面的重试监听器。
通过以下三方法可以自定义各状态页面,并且调用立刻生效且不会影响当前数据显示状态:

    public void customizeEmptyView(View view) {
        mNetStateView.customizeEmptyView(view);
    }
    public void customizeLoadingView(View view) {
        mNetStateView.customizeLoadingView(view);
    }
    public void customizeErrorView(View view) {
        mNetStateView.customizeErrorView(view);
    }

Pullable接口

任何放入PullToRefreshLayout作为innerView的控件都需要实现Pullable接口,使得容器能够判断innerView是否能够进行pullDown和pullUp动作。innerView需要借此控制是否能够进行下拉或上拉操作,返回false则无法进行对应的操作。一般情况下,实现Pullable接口作为innerView的视图控件还要处理与PullToRefreshLayout的滑动事件分发,这个后面再说。

public interface Pullable {
    boolean canPullDown();
    boolean canPullUp();
}

PullableRecyclerView

介绍了Pullable接口,下面介绍主要成员——PullableRecyclerView。类图如下:


PullableRecyclerView继承关系

作为下拉刷新的主体View,它需要具备的功能包含:显示数据不同状态页面(Empty、Error、Loading及Normal);在Normal状态下,RecyclerView上拉至顶部的下拉刷新及下拉至分页处的上拉加载;Empty状态下的下拉刷新。

  1. 为做到以上几点,PullableRecyclerView继承NetableRecyclerView,实现Pullable接口。
  2. 功能管理。
    //初始化
    private boolean mCanRefresh = true;
    private boolean mCanLoad = true;

    private boolean mAllowRefresh = true;
    private boolean mAllowLoad = true;

为了适应多种场景下的使用,设置了setAllowRefresh(boolean allowRefresh)setAllowLoad(boolean allowLoad)方法,用来控制是否启用上拉下拉的能力,即只有(allowRefresh&&mCanRefresh)为true才能够进入下拉状态,Load同理。

  1. 重写了dispatchTouchEvent(MotionEvent ev),但没有影响任何触摸事件传递,只不过是在MotionEvent为MOVE_DOWN的时候进行了是否进入上拉或下拉状态的判断(mCanRefresh和mCanLoad)。
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (mNetStateView.getNetState()) {
            case NetStateView.DATA_STATUS_EMPTY:
                mCanRefresh = true;
                mCanLoad = false;
                break;
            case NetStateView.DATA_STATUS_ERROR:
                mCanRefresh = false;
                mCanLoad = false;
                break;
            case NetStateView.DATA_STATUS_LOADING:
                mCanRefresh = false;
                mCanLoad = false;
                break;
            case NetStateView.DATA_STATUS_NORMAL:
                if ((((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition()) == 0) {
                    mCanRefresh = true;
                } else {
                    mCanRefresh = false;
                }

                if ((((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastCompletelyVisibleItemPosition()) == getAdapter().getItemCount() - 1) {
                    mCanLoad = true;
                } else {
                    mCanLoad = false;
                }
                break;
        }


        return super.dispatchTouchEvent(ev);
    }

这就需要保证在MOVE_DOWN事件发生时,ViewGroup不能拦截,而要允许其透传到子View的dispatchTouchEvent中。至于PullToRefreshLayout中如何做到,详见PullToRefreshLayout

PullToRefreshLayout

最后介绍最最重要的一个ViewGroup——封装了下拉和上拉的操作的PullToRefreshLayout。作为一个容器,可在xml中按顺序加入三个子view(headerView,innerView及footerView)。使用如下,示例中加入了按照上述原理封装好的WebView作为innerView:

    
        
        
        
    
    

在PullToRefreshLayout首次onLayout渲染的时候通过getChildAt()获取内部View,依次得到headerView,innerView及footerView。
在PullToRefreshLayout中实现了如下功能:

  • 判断是否需要拦截触摸事件
  • 拦截触摸事件后,处理下拉或上拉视图
  • 下拉、上拉过程的状态和动画效果
    为做到第一点,需要重写onInterceptTouchEvent()方法,MotionEvent.ACTION_DOWN时,不进行任何拦截,使得动作能够透传至子View中(PullableRecyclerView的dispatchTouchEvent方法能够得到调用)如下:
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean rst = false;   //  默认不拦截
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: //  按下事件,不拦截
                downY = ev.getY();
                lastY = downY;
                downX = ev.getX();
                lastX = downX;
                break;
            case MotionEvent.ACTION_MOVE: 
                //若纵向滑动偏移量大于横向滑动偏移量,忽略横向滑动;解决了既有纵向滑动又有横向滑动的过敏问题(比如:item的横向滑动删除效果,如果没有该判断,将会很容易在斜滑的时候触发横向逻辑)
                if (Math.abs(ev.getX() - lastX) < Math.abs(ev.getY() - lastY)) {
                    if (ev.getY() > lastY) {
                        //若innerView处于canPullDown状态、或当前状态为刷新中或加载中,则触摸事件被拦截下来,由该类自行控制,不再分发给子view。
                        if (((Pullable) pullableView).canPullDown() || state == REFRESHING || state == LOADING)
                            rst = true;
                        else {
                            rst = false;
                        }
                    } else {
                        //同下拉刷新
                        if (((Pullable) pullableView).canPullUp() || state == LOADING || state == REFRESHING) {
                            rst = true;
                        } else {
                            rst = false;
                        }
                    }
                } else 
                      return false;
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return rst;
    }

第二点和第三点其实是一回事,即在触摸事件拦截下来后,控制权掌握在了ViewGroup自己手里,如何处理滑动动效及当前视图状态的问题。

  1. 重写onTouchEvent()处理触摸态下视图更改。
  2. 处理手松开后,视图的更改,借助Timer、Handler、Task实现。(具体实现方式以后再讲)

PullToRefreshRecyclerView

PullToRefreshRecyclerView类图

继承PullToRefreshLayout,封装了一套默认header和footer布局,并以PullableRecyclerView为innerView。布局如下:


    

    

    
    


PullToRefreshRecyclerView 初始化直接使用的是xml布局渲染的方式,定制了一套header和footer布局。merge之后,该类本身即为xml布局文件中三个子view的父布局。因此在PullToRefreshLayout首次onLayout获取子view的时候即可拿到对应内容。

源码链接

你可能感兴趣的:(PullToRefreshRecyclerView封装实现)