為什么要使用組合的方式
Android下拉刷新和上拉加載的框架網(wǎng)上已經(jīng)非常多了,但是大多數(shù)都需要繼承自定義的下拉SwiperefreshLayout
速梗,或者自定義recyclerview
, 或者自定的adapter
。主要有以下幾種:
- 必須要繼承自定義的
xxRefreshLayout
襟齿。 將下拉刷新控件及listview
整個(gè)封裝到一個(gè)控件里姻锁。 - 必須繼承自定義的
xxListView
。將上拉加載功能封裝到自定義的listview
或者RecyclerView
里猜欺。 - 必須繼承作者自定義的
Adapter
位隶。
無論哪種我都不愿意使用。我可能有自己的下拉控件开皿,也可能直接使用原生的的SwipeRefreshLayout
(會(huì)有一些定制修改)涧黄。列表部分雖然大部分情況下使用RecyclerView
,但是有的地方也用到了ListView
赋荆。至于Adapter
,我更是不會(huì)去繼承了笋妥。相信大家都有自己的BaseAdapter
,雖然大同小異窄潭,但是也有獨(dú)特和不同的地方春宣,為了LoadMore去繼承修改自己的BaseAdapter
很糟糕。 比如我的BaseAdapter
封裝了click事件嫉你,并基于DataBinding
月帝,連ViewHolder
都省了。
從代碼設(shè)計(jì)上考慮均抽,繼承并不是很好的選擇嫁赏,代碼入侵性太強(qiáng)。還是優(yōu)先使用基礎(chǔ)控件油挥,除非是Google等知名第三方庫潦蝇。這里我更傾向于使用組合包裝的方式實(shí)現(xiàn)LoadMoreHelper
款熬。下拉控件可能是PtrFrameLayout
也可能是SwiperefreshLayout
, 列表控件可能是ListView
也可能是RecyclerView
,使用自己的BaseAdapater
即可攘乒。LoadMoreHelper
并不是自定義控件贤牛,而是通過數(shù)據(jù)Loader,對各個(gè)組件進(jìn)行設(shè)置则酝。而對于各個(gè)組件來說不需要依賴LoadMoreHelper
殉簸。
</br>
下拉刷新與上拉加載過程分析
雖然兩者看起來很相似,但是還是有區(qū)別沽讹。一些控件將兩種行為強(qiáng)行整合到一起是不恰當(dāng)?shù)陌惚啊K砸话阆吕⑿驴丶疾粫?huì)提供上拉加載功能,需要用戶自己去實(shí)現(xiàn)
下拉刷新和上拉加載是兩種不同的控件功能爽雄,不要混淆在一起蝠检。
目前最好用的是Google的SwipeRefreshLayout
,有的工程還用到了仿ios的PtrFrameLayout
挚瘟,盡管該控件不再維護(hù)更新了叹谁,但是是否使用是取決于開發(fā)者,LoadMoreHelper
不做限制乘盖。下拉樣式及處理滑動(dòng)事件都是由下拉控件負(fù)責(zé)定制和處理焰檩,LoadMoreHelper
不用關(guān)心這些。
先看一下數(shù)據(jù)加載的大致流程:
雖然下拉刷新和上拉加載在UI體驗(yàn)上不同订框,但是其裝載數(shù)據(jù)的過程卻差不多
下拉刷新load第1頁數(shù)據(jù)析苫,并替換當(dāng)前全部list數(shù)據(jù)
上拉加載load大于第1頁的數(shù)據(jù),并將加載的數(shù)據(jù)補(bǔ)充到當(dāng)前l(fā)ist里
調(diào)用者必須實(shí)現(xiàn)數(shù)據(jù)加載接口布蔗,獲取數(shù)據(jù)結(jié)果藤违,并通過數(shù)據(jù)裝載器通知UI變化。
-
數(shù)據(jù)加載接口
可以是同步纵揍,可以是異步顿乒。
同步數(shù)據(jù)加載接口SyncDataLoader
直接返回?cái)?shù)據(jù)結(jié)果,LoadMoreHelper
會(huì)在后臺線程調(diào)用該接口;
異步數(shù)據(jù)加載接口AsyncDataLoader
泽谨,在數(shù)據(jù)加載成功后的回調(diào)用LoadMoreHelper.onLoadEnd
璧榄,通知數(shù)據(jù)結(jié)果/** * Load data sync. LoadHelper will call it on work thread */ @WorkerThread public interface SyncDataLoader<VM> { PageData<VM> startLoadData(int page, PageData<VM> lastPageData); } /** * Load data int async thread */ public interface AsyncDataLoader<VM> { void startLoadData(int page, PageData<VM> lastPageData); }
</br>
-
數(shù)據(jù)結(jié)果PageData的定義,
主要包含吧雹,1.pageIndex,當(dāng)前頁碼骨杂,2. list,數(shù)據(jù),3. pageMore,是否還有下一頁數(shù)據(jù)雄卷,4. result,是否加載成功public final class PageData<DT> { private int pageIndex; private List<DT> data; private boolean pageMore; private boolean success = true; }
-
數(shù)據(jù)裝載接口
數(shù)據(jù)裝載的接口定義搓蚪,IDataSwapper
,在下拉刷新時(shí)調(diào)用swapData, 在上拉加載時(shí)調(diào)用appendData。建議使用Adapter
繼承該接口丁鹉,當(dāng)然也可以單獨(dú)實(shí)現(xiàn)妒潭。public interface IDataSwapper<VM> { /** * Swap all datas */ void swapData(List<? extends VM> list); /** * Append data to the end of current list */ void appendData(List<? extends VM> list); }
使用裝飾模式添加加載更多view
由于RecyclerView
不能像ListView
那樣直接添加headerview或者footerview悴能,需要自己在Adapter
里實(shí)現(xiàn)。而這里我們又并不想繼承該Adapter
雳灾,使用裝飾模式將調(diào)用者的Adapter
包裝起來漠酿。這樣對調(diào)用者來說,只需要關(guān)注自己的Adapter
即可谎亩,不需要關(guān)注包裝者炒嘲。
我們先看一下Android原生的ListView
是如何實(shí)現(xiàn)header和footer
在添加footer后,會(huì)將原adapter包裝成HeaderViewListAdapter
public void addFooterView(View v, Object data, boolean isSelectable) {
...
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
wrapHeaderListAdapterInternal();
}
...
}
}
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
private final ListAdapter mAdapter;
...
}
參考該設(shè)計(jì)匈庭,類似的我們在添加RecyclerView
的footerView時(shí),也可以包裝一個(gè)FooterAdapter
//包裝origin adapter
RecyclerView.Adapter<VH> originAdapter = recyclerView.getAdapter();
footerAdapter = new FooterViewAdapter<>(originAdapter);
recyclerView.setAdapter(footerAdapter);
FooterAdapter
會(huì)將原來的Adapter包裝起來夫凸,并在尾部添加 加載更多的view
//FooterAdapter 繼承于BaseWrapperAdapter
public class FooterViewAdapter<VH extends RecyclerView.ViewHolder> extends BaseWrapperAdapter
public class BaseWrapperAdapter<VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
private RecyclerView.Adapter<VH> mWrappedAdapter;
...
}
如此,便可以不要求用戶繼承LoadMoreHelper
的Adapter
嚎花,又能在RecyclerView
的末尾添加footerView寸痢。對于調(diào)用者來說,這一層完全是透明的紊选,只需要按照之前的習(xí)慣調(diào)用原Adapter
的notifyDataSetChanged
即可,不需要關(guān)注包裝者的存在道逗。
LoadMoreHelper調(diào)用形式
布局文件使用原生的控件即可兵罢。其中SwipeRefreshLayout
可替換成PtrFrameLayout
,RecyclerView
可替換成ListView
.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
Api采用鏈?zhǔn)秸{(diào)用。推薦使用lambda表達(dá)式使代碼更簡潔滓窍。
-
首先設(shè)置
swipeRefreshLayout
,會(huì)自動(dòng)需要包含的RecyclerView
卖词。 如果不需要下拉刷新只用上拉加載的話,可以直接設(shè)置RecyclerView
吏夯。在LoadMoreHelper
內(nèi)部此蜈,會(huì)根據(jù)傳入的view進(jìn)行設(shè)置,設(shè)置RecyclerView
的滑動(dòng)監(jiān)聽噪生,包裝Adapter
等裆赵。loadHelper = LoadMoreHelper.create(swipeRefreshLayout) .setDataSwapper(adapter) .setAsyncDataLoader((page, lastPageData) -> doLoadData(page)) .startPullData(true);
-
設(shè)置
IDataSwapper
。這里Adapter實(shí)現(xiàn)了IDataSwapper
接口跺嗽。當(dāng)然可以不使用Adapter繼承而單獨(dú)實(shí)現(xiàn)該接口战授,具體可參見工程里的例子。private class MyAdapter2 extends RecyclerView.Adapter<ViewHolder2> implements IDataSwapper<Item> {
...
@Override
public void swapData(List<? extends Item> list) {
datas.clear();
datas.addAll(list);
notifyDataSetChanged();
}@Override public void appendData(List<? extends Item> list) { if (list == null || list.isEmpty()) { return; } int start = datas.size(); datas.addAll(list); notifyItemRangeInserted(start, list.size()); }
}
-
設(shè)置
AsyncDataLoader
桨嫁。 采用異步數(shù)據(jù)加載植兰,這里的loadData
使用了Rxjava
做例子。LoadMoreHelper
會(huì)在下拉刷新或者上拉加載時(shí)調(diào)用startLoadData
璃吧。通知當(dāng)前所需數(shù)據(jù)的頁碼楣导,調(diào)用者只需填充加載方法,并將數(shù)據(jù)加載結(jié)果通過onLoadEnd
傳入畜挨。new LoadMoreHelper.AsyncDataLoader<Item>(){ @Override public void startLoadData(int page, PageData lastPageData) { DataLoader.loadData(pageIndex, null) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { PageData<Item> pageData = PageData.createSuccess(pageIndex, result.getData(), result.isPageMore()); loadHelper.onLoadEnd(pageData); }, e -> { PageData<Item> pageData = PageData.createFailed(pageIndex); loadHelper.onLoadEnd(pageData); }); } };
LoadMoreHelper
也提供了一些其他接口可以設(shè)置加載更多viewsetLoadMoreViewCreator
筒繁,加載失敗viewsetLoadFailedViewCreator
彬坏,加載完畢viewsetLoadCompleteViewCreator
。頁面數(shù)據(jù)加載失敗后可以點(diǎn)擊重試膝晾。