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

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。類圖如下:

作為下拉刷新的主體View,它需要具備的功能包含:顯示數(shù)據(jù)不同狀態(tài)頁(yè)面(Empty、Error、Loading及Normal);在Normal狀態(tài)下,RecyclerView上拉至頂部的下拉刷新及下拉至分頁(yè)處的上拉加載;Empty狀態(tài)下的下拉刷新。
- 為做到以上幾點(diǎn),PullableRecyclerView繼承NetableRecyclerView,實(shí)現(xiàn)Pullable接口。
- 功能管理。
//初始化
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同理。
- 重寫了
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)題。
- 重寫
onTouchEvent()處理觸摸態(tài)下視圖更改。 - 處理手松開(kāi)后,視圖的更改,借助Timer、Handler、Task實(shí)現(xiàn)。(具體實(shí)現(xiàn)方式以后再講)
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)容。