PullToRefreshRecyclerView封裝實(shí)現(xiàn)

原生RecyclerView無(wú)法支持下拉刷新及上拉加載等操作,需要封裝才能支持??紤]到不僅僅是RecyclerView可能需要該操作,任何一個(gè)View都有可能需要,因此將上下拉設(shè)計(jì)為一個(gè)可容納三個(gè)子View的容器(headerView,innerView和footerView)。

PullToRefreshRecyclerView總體思路

NetableView

封裝了三個(gè)狀態(tài)view(Loading、Empty、Error)并從外部傳入一個(gè)innerView(可以是任意View,作為內(nèi)容顯示的view)??赏ㄟ^(guò)setNetState(int state)控制狀態(tài)頁(yè)面的展示。狀態(tài)類型如下:

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

NetableRecyclerView

組合了RecyclerView及NetStateView,并將RecyclerView傳入NetStateView以進(jìn)行狀態(tài)統(tǒng)一管控。通過(guò)提供的notifyNetState(int state)可直接更新頁(yè)面數(shù)據(jù)狀態(tài)。setDefaultRetryClickListener()可設(shè)置默認(rèn)Error頁(yè)面的重試監(jiān)聽(tīng)器。
通過(guò)以下三方法可以自定義各狀態(tài)頁(yè)面,并且調(diào)用立刻生效且不會(huì)影響當(dāng)前數(shù)據(jù)顯示狀態(tài):

    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的控件都需要實(shí)現(xiàn)Pullable接口,使得容器能夠判斷innerView是否能夠進(jìn)行pullDown和pullUp動(dòng)作。innerView需要借此控制是否能夠進(jìn)行下拉或上拉操作,返回false則無(wú)法進(jìn)行對(duì)應(yīng)的操作。一般情況下,實(shí)現(xiàn)Pullable接口作為innerView的視圖控件還要處理與PullToRefreshLayout的滑動(dòng)事件分發(fā),這個(gè)后面再說(shuō)。

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

PullableRecyclerView

介紹了Pullable接口,下面介紹主要成員——PullableRecyclerView。類圖如下:


PullableRecyclerView繼承關(guān)系

作為下拉刷新的主體View,它需要具備的功能包含:顯示數(shù)據(jù)不同狀態(tài)頁(yè)面(Empty、Error、Loading及Normal);在Normal狀態(tài)下,RecyclerView上拉至頂部的下拉刷新及下拉至分頁(yè)處的上拉加載;Empty狀態(tài)下的下拉刷新。

  1. 為做到以上幾點(diǎn),PullableRecyclerView繼承NetableRecyclerView,實(shí)現(xiàn)Pullable接口。
  2. 功能管理。
    //初始化
    private boolean mCanRefresh = true;
    private boolean mCanLoad = true;

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

為了適應(yīng)多種場(chǎng)景下的使用,設(shè)置了setAllowRefresh(boolean allowRefresh)setAllowLoad(boolean allowLoad)方法,用來(lái)控制是否啟用上拉下拉的能力,即只有(allowRefresh&&mCanRefresh)為true才能夠進(jìn)入下拉狀態(tài),Load同理。

  1. 重寫了dispatchTouchEvent(MotionEvent ev),但沒(méi)有影響任何觸摸事件傳遞,只不過(guò)是在MotionEvent為MOVE_DOWN的時(shí)候進(jìn)行了是否進(jìn)入上拉或下拉狀態(tài)的判斷(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事件發(fā)生時(shí),ViewGroup不能攔截,而要允許其透?jìng)鞯阶覸iew的dispatchTouchEvent中。至于PullToRefreshLayout中如何做到,詳見(jiàn)PullToRefreshLayout。

PullToRefreshLayout

最后介紹最最重要的一個(gè)ViewGroup——封裝了下拉和上拉的操作的PullToRefreshLayout。作為一個(gè)容器,可在xml中按順序加入三個(gè)子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渲染的時(shí)候通過(guò)getChildAt()獲取內(nèi)部View,依次得到headerView,innerView及footerView。
在PullToRefreshLayout中實(shí)現(xiàn)了如下功能:

  • 判斷是否需要攔截觸摸事件
  • 攔截觸摸事件后,處理下拉或上拉視圖
  • 下拉、上拉過(guò)程的狀態(tài)和動(dòng)畫效果
    為做到第一點(diǎn),需要重寫onInterceptTouchEvent()方法,MotionEvent.ACTION_DOWN時(shí),不進(jìn)行任何攔截,使得動(dòng)作能夠透?jìng)髦磷覸iew中(PullableRecyclerView的dispatchTouchEvent方法能夠得到調(diào)用)如下:
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean rst = false;   //  默認(rèn)不攔截
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: //  按下事件,不攔截
                downY = ev.getY();
                lastY = downY;
                downX = ev.getX();
                lastX = downX;
                break;
            case MotionEvent.ACTION_MOVE: 
                //若縱向滑動(dòng)偏移量大于橫向滑動(dòng)偏移量,忽略橫向滑動(dòng);解決了既有縱向滑動(dòng)又有橫向滑動(dòng)的過(guò)敏問(wèn)題(比如:item的橫向滑動(dòng)刪除效果,如果沒(méi)有該判斷,將會(huì)很容易在斜滑的時(shí)候觸發(fā)橫向邏輯)
                if (Math.abs(ev.getX() - lastX) < Math.abs(ev.getY() - lastY)) {
                    if (ev.getY() > lastY) {
                        //若innerView處于canPullDown狀態(tài)、或當(dāng)前狀態(tài)為刷新中或加載中,則觸摸事件被攔截下來(lái),由該類自行控制,不再分發(fā)給子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;
    }

第二點(diǎn)和第三點(diǎn)其實(shí)是一回事,即在觸摸事件攔截下來(lái)后,控制權(quán)掌握在了ViewGroup自己手里,如何處理滑動(dòng)動(dòng)效及當(dāng)前視圖狀態(tài)的問(wèn)題。

  1. 重寫onTouchEvent()處理觸摸態(tài)下視圖更改。
  2. 處理手松開(kāi)后,視圖的更改,借助Timer、Handler、Task實(shí)現(xiàn)。(具體實(shí)現(xiàn)方式以后再講)

PullToRefreshRecyclerView

PullToRefreshRecyclerView類圖

繼承PullToRefreshLayout,封裝了一套默認(rèn)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布局文件中三個(gè)子view的父布局。因此在PullToRefreshLayout首次onLayout獲取子view的時(shí)候即可拿到對(duì)應(yīng)內(nèi)容。

源碼鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,741評(píng)論 25 709
  • Android事件傳遞機(jī)制一直都是一個(gè)痛點(diǎn),希望這篇文章能夠給你點(diǎn)不一樣的 基礎(chǔ)知識(shí)—>源碼分析—>進(jìn)階—>應(yīng)用場(chǎng)...
    李是猴子搬來(lái)的救兵閱讀 2,976評(píng)論 5 13
  • layout: postdate: 2016-01-08title: Android開(kāi)發(fā)藝術(shù)探索-第三章-View...
    KuTear閱讀 1,965評(píng)論 2 18
  • 簡(jiǎn)介: 提供一個(gè)讓有限的窗口變成一個(gè)大數(shù)據(jù)集的靈活視圖。 術(shù)語(yǔ)表: Adapter:RecyclerView的子類...
    酷泡泡閱讀 5,358評(píng)論 0 16
  • 我們驅(qū)車沿著蜿蜒曲折的盤山公路行駛,沿途崇山峻嶺,奇峰異石,倚高山而建的山上人家,讓我大開(kāi)眼界!想象中遠(yuǎn)近聞名的...
    況元閱讀 922評(píng)論 2 3

友情鏈接更多精彩內(nèi)容