PullToRefreshRecyclerView封装实现

作者: GhostInMatrix | 来源:发表于2017-06-01 19:48 被阅读0次

    原生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:

        <com.baidu.lbs.widget.PullToRefreshLayout
            android:id="@+id/pull_to_refresh_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >
            <include
                android:id="@+id/pull_header"
                layout="@layout/refresh_head"/>
            <com.baidu.lbs.commercialism.bridge.WMWebView
                android:id="@+id/common_webview"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            <include
                android:id="@+id/pull_footer"
                layout="@layout/load_more"/>
        </com.baidu.lbs.widget.PullToRefreshLayout>
        
    

    在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。布局如下:

    <merge
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <include
            android:id="@+id/recyclerview_header"
            layout="@layout/refresh_head"/>
    
        <com.baidu.lbs.widget.recyclerview.PullableRecyclerView
            android:id="@+id/pullable_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/transparent"
            android:divider="@null"
            android:dividerPadding="0dp"
            android:showDividers="none"
            >
    
        </com.baidu.lbs.widget.recyclerview.PullableRecyclerView>
        <include
            android:id="@+id/recyclerview_footer"
            layout="@layout/load_more_2"/>
    
    </merge>
    

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

    源码链接

    相关文章

      网友评论

        本文标题:PullToRefreshRecyclerView封装实现

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