下拉刷新分頁加載控件分析
主流下拉刷新控件橫評
備注:我將從實(shí)現(xiàn)原理、易用性、擴(kuò)展性、穩(wěn)定性三個(gè)方面比較
易用性:包括 1、使用是否方便,xml java均可配置使用 2、是否將常用的邏輯功能封裝(分頁計(jì)算、footer等),使用者不關(guān)心細(xì)節(jié) 3、對一些常用的擴(kuò)展是否已支持可配置(header的樣式等)
擴(kuò)展性:包括 1、支持的下拉、分頁的ViewGroup是否可方便擴(kuò)展 2、header footer等是否擴(kuò)展方便
穩(wěn)定性:包括 1、github活躍性,issue是否及時(shí)處理 2、上線后控件內(nèi)部crash
一、最早的先行者:XListView (https://github.com/Maxwin-z/XListView-Android)
1、實(shí)現(xiàn)原理:
直接extends ListView,使用也和Listview一樣,header和footer也是采用ListView自帶的功能,僅對layout做了封裝XListViewFooter和XListViewHeader。
從代碼結(jié)構(gòu)來看,非常簡單。header和footer的顯示,通過listview的onTouchEvent來判斷。
[圖片上傳失敗...(image-b71760-1541780666346)]
2、易用性:與ListView同,但是下拉和分頁的可配置性幾乎沒有,常用封裝全無
3、擴(kuò)展性:很差,只能在使用ListView時(shí)使用,擴(kuò)展需要改動代碼,代碼本身擴(kuò)展性考慮很少。
4、穩(wěn)定性:github已停更,線上經(jīng)典crash難于解決。
作為最早Android下拉刷新功能的實(shí)踐者,僅有有歷史意義
二、廣泛應(yīng)用者:PullToRefresh (https://github.com/chrisbanes/Android-PullToRefresh)
1、實(shí)現(xiàn)原理:
其類圖可以較好的說明,其架構(gòu)方式:
[圖片上傳失敗...(image-4335ec-1541780666347)]
PullToRefresh控件基本奠定了 下拉刷新控件的架構(gòu)形式:架構(gòu)兩大部分,
1)一部分下拉分頁的骨架:核心content的加載和擴(kuò)展、footer和header的加載、交互(state的分發(fā))等
2)一部分footer和header的處理:footer header架構(gòu)對不同state的處理,及自身的擴(kuò)展和定制。
依據(jù)以上兩部分,基于IPullToRefresh和 ILoadingLayout兩個(gè)接口開發(fā)。
- 核心骨架
private void init(Context context, AttributeSet attrs) {
........//init Codes
setGravity(Gravity.CENTER);
ViewConfiguration config = ViewConfiguration.get(context);
mTouchSlop = config.getScaledTouchSlop();
....//Parse styleable
// Refreshable View 用于擴(kuò)展
// By passing the attrs, we can add ListView/GridView params via XML
mRefreshableView = createRefreshableView(context, attrs);
addRefreshableView(context, mRefreshableView);
// We need to create now layouts now
//createLoadingLayout方法構(gòu)造header 和 footer
mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);
if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
}
if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
mScrollingWhileRefreshingEnabled = a.getBoolean(
R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false);
}
// Let the derivative classes have a go at handling attributes, then
// recycle them...
handleStyledAttributes(a);
a.recycle();
// Finally update the UI for the modes
//updateUIForMode 用于添加footer和header到linearlayout中
updateUIForMode();
}
PullToRefreshBase本身是LinearLayout,其支持橫向(很少用)和縱向的下拉刷新,把contentView(mRefreshableView)和footer header作為childView添加到其中。
擴(kuò)展方式:
abstract方法createRefreshableView(),在子類中實(shí)現(xiàn)用于擴(kuò)展contentView
footer header的擴(kuò)展通過createLoadingLayout()返回,只要繼承自LoadingLayout即可擴(kuò)展。當(dāng)然控件本身提供了集中常用的Loadinglayout(FlipLoadingLayout RotateLoadingLayout)
交互處理:
如何從手勢的變化決定header以及footer的state呢?是通過onInterceptTouchEvent和OnTouchEvent。
和其他的touch事件處理類似,onInterceptTouchEvent方法作為前置準(zhǔn)備,onTouchEvent方法實(shí)際處理手勢操作
@Override
public final boolean onTouchEvent(MotionEvent event) {
if (!isPullToRefreshEnabled()) {
return false;
}
// If we're refreshing, and the flag is set. Eat the event
if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
return true;
}
if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
if (mIsBeingDragged) {
mLastMotionY = event.getY();
mLastMotionX = event.getX();
pullEvent();//處理拉動過程中,header footer狀態(tài)的變化
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = mInitialMotionX = event.getX();
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
//ACTION_UP事件的處理,在不同state下松手,處理方式的不同
if (mIsBeingDragged) {
mIsBeingDragged = false;
if (mState == State.RELEASE_TO_REFRESH
&& (null != mOnRefreshListener || null != mOnRefreshListener2)) {
//拉動結(jié)束,在RELEASE_TO_REFRESH狀態(tài)下松手,變?yōu)镽EFRESHING
setState(State.REFRESHING, true);
return true;
}
// If we're already refreshing, just scroll back to the top
if (isRefreshing()) {
//拉動結(jié)束,在REFRESHING狀態(tài)下松手,回到原點(diǎn)
smoothScrollTo(0);
return true;
}
// If we haven't returned by here, then we're not in a state
// to pull, so just reset
//拉動結(jié)束,在其他狀態(tài)(PULL_TO_REFRESH)下松手,reset到初始狀態(tài)
setState(State.RESET);
return true;
}
break;
}
}
return false;
}
/**
* Actions a Pull Event
*
* @return true if the Event has been handled, false if there has been no
* change
*/
private void pullEvent() {
final int newScrollValue;
final int itemDimension;
final float initialMotionValue, lastMotionValue;
switch (mCurrentMode) {
case PULL_FROM_END:
newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
itemDimension = getFooterSize();
break;
case PULL_FROM_START:
default:
newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
itemDimension = getHeaderSize();
break;
}
setHeaderScroll(newScrollValue);
if (newScrollValue != 0 && !isRefreshing()) {
float scale = Math.abs(newScrollValue) / (float) itemDimension;
switch (mCurrentMode) {
case PULL_FROM_END://上拉分頁
mFooterLayout.onPull(scale);//根據(jù)滑動的位置更新footerLayout
break;
case PULL_FROM_START://下拉刷新
default:
mHeaderLayout.onPull(scale);//根據(jù)滑動的位置更新headerLayout
break;
}
//根據(jù)滑動的位置(是否超過閾值),決定狀態(tài)PULL_TO_REFRESH or RELEASE_TO_REFRESH
if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
setState(State.PULL_TO_REFRESH);
} else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
setState(State.RELEASE_TO_REFRESH);
}
}
}
從以上代碼可以看到,從手指開始滑動控件的不同狀態(tài),PullToRefreshBase的不同狀態(tài)的流轉(zhuǎn),在流轉(zhuǎn)的過程中,不只更新了state狀態(tài),也對footer和header進(jìn)行了同步。
從以上代碼容易理解下拉刷新的邏輯脈絡(luò),但是上拉分頁加載是怎么實(shí)現(xiàn)的呢?
PullToRefreshBase控件通過mCurrentMode來區(qū)分上拉和下拉,其實(shí)上拉和下拉的邏輯,從整體上是可以歸一的,有幾個(gè)關(guān)鍵點(diǎn)
1、判斷上拉 下拉的邏輯閾值:isReadyForPullStart()isReadyForPullEnd()分別是下拉 上拉的閾值方法,子類需要根據(jù) mRefreshableView來實(shí)現(xiàn)
2、在不同的state下做不同的處理: 兩者都有 reset PULL_TO_REFRESH RELEASE_TO_REFRESH REFRESHING等狀態(tài),可能上拉不需要區(qū)分PULL_TO_REFRESH RELEASE_TO_REFRESH兩種state而已。所以既然都是基于一套state的處理方案,那么根據(jù)手勢滑動方向決定當(dāng)前mCurrentMode,進(jìn)而交給header 或 footer來處理state就是可行的。
- footer和header的擴(kuò)展和處理
剛才說到了footer和header是在同一套state狀態(tài)下的處理機(jī)制,其回調(diào)也類似。所以兩者繼承同一接口和基類。PullToRefreshBase控件采用了Proxy的方式,實(shí)現(xiàn)了二者的統(tǒng)一調(diào)用。
也就是說LoadingLayoutProxy 、headerLoadingLayout、footerLoadingLayout均實(shí)現(xiàn)ILoadingLayout,LoadingLayoutProxy是headerLoadingLayout與footerLoadingLayout二者的代理,在state的流轉(zhuǎn)過程中,通過LoadingLayoutProxy的調(diào)用,達(dá)到header 和footer兩個(gè)loadingLayout的同步調(diào)用。
LoadingLayout基類已經(jīng)實(shí)現(xiàn)了基本的layout,我們自己定制的子類(例如CustomLoadingLayout),對里面的動畫,文案等進(jìn)行定制即可,基于ILoadingLayout接口完全重寫一個(gè)新的,目前看不行,一方面PullToRefreshBase控件內(nèi)部很多地方強(qiáng)轉(zhuǎn)到LoadingLayout。而且LoadingLayout基類(abstract類)預(yù)留了stated的回調(diào)抽象方法,供子類實(shí)現(xiàn):
protected abstract void onLoadingDrawableSet(Drawable imageDrawable);
protected abstract void onPullImpl(float scaleOfLayout);
protected abstract void pullToRefreshImpl();
protected abstract void refreshingImpl();
protected abstract void releaseToRefreshImpl();
protected abstract void resetImpl();
2、易用性:
實(shí)現(xiàn)原理說了這么多,基本上把控件的基本架構(gòu)和處理流程都涉及了。一般來說,通用控件架構(gòu)設(shè)計(jì)的初衷一般為易用性和擴(kuò)展性考慮。好的架構(gòu)能夠兼顧這二者。我們具體看一下:
1、使用是否方便,xml和java代碼都可以初始化和配置控件,這是控件設(shè)計(jì)初期就考慮到的
2、我們知道為了保證擴(kuò)展性,架構(gòu)上的實(shí)現(xiàn)不能過于具體,否則靈活性降低。架構(gòu)上基于接口和抽象類進(jìn)行設(shè)計(jì),能保證在整體架構(gòu)內(nèi)部方便擴(kuò)展。同時(shí)也提供了一些常用的具體實(shí)現(xiàn)類,比如PullToRefreshListView FlipLoadingLayout等對于一般的使用者可以省去二次開發(fā)的時(shí)間
3、一些業(yè)務(wù)上的常用邏輯:(分頁計(jì)算、footer多個(gè)狀態(tài)的顯示等)沒有集成,需要二次開發(fā)
3、擴(kuò)展性:
mRefreshableView的設(shè)計(jì)理念,可以說讓控件理論上可以支持任何視圖類(ViewGroup)的下拉刷新操作,比如后期擴(kuò)展RecyclerView、ViewPager等。
從類圖中可以看出 PullToRefreshBase的多層子類,設(shè)計(jì)合理,層次分明。二次開發(fā)中可以選擇合適的基類進(jìn)行擴(kuò)展。
LoadingLayoutProxy機(jī)制的引入,為實(shí)現(xiàn)更多LoadingLayout的state流轉(zhuǎn)提供了可能。
模板方法設(shè)計(jì)模式,基于接口開發(fā),abstract基類,易于擴(kuò)展和維護(hù)
4、穩(wěn)定性:
github star 8700多,多個(gè)工程中考驗(yàn),類庫內(nèi)部崩潰率較低。
三、官方控件:SwipeRefreshLayout
一兩句就能說清:
這個(gè)控件作為targetView(比如listview)的parentView出現(xiàn),而且SwipeRefreshLayout只能有一個(gè)childView。
交互上比較單一,materialDesign風(fēng)格,loading圖標(biāo)在targetView之上顯示,targetView本身可以是任何view。
四、二次開發(fā)的控件:LRecyclerView
LRecyclerView是csdn大牛‘一葉飄舟’所著,設(shè)計(jì)的初衷是為了打造一個(gè)更為好用的RecyclerView,一切基于RecyclerView架構(gòu)搭建。增加了header footer功能(不同于listview,為了擴(kuò)展性,原生的RecyclerView并不支持header和footer)。增加了下拉刷新和上拉分頁加載功能(這個(gè)功能后來被更廣泛使用,所以在已有架構(gòu)上支持了PullScrollView、PullWebView)。最終達(dá)到了現(xiàn)有的面貌。
目前我們已經(jīng)將RecyclerView作為開發(fā)的主力控件,那么基于RecyclerView的一個(gè)易用性、擴(kuò)展性和穩(wěn)定性逗號的控件,就是我們研究的目標(biāo)。
1、實(shí)現(xiàn)原理:
有了以上的背景,我們對LRecyclerView這個(gè)控件會有一個(gè)大概認(rèn)識。我們看下代碼分布:
[圖片上傳失敗...(image-ea4ae6-1541780666347)]
從他的代碼分布可以看出,基本是圍繞LRecyclerview開展的。類之間的相互關(guān)系比較簡單,就不用類圖展開了。
LRecyclerView是主體類,核心代碼在LRecyclerView中。(另外LuRecyclerView是為了適應(yīng)google官方控件SwipeRefreshLayout而改造的,因?yàn)橹苯邮褂肧wipeRefreshLayout嵌套LRecyclerView會有事件沖突。在分析上可以暫時(shí)略過)。
以下我們將從兩個(gè)方面分析 1、LRecyclerView是如何在RecyclerView基礎(chǔ)上加上footer和header;2、LRecyclerView是如何實(shí)現(xiàn)下拉刷新和上拉分頁加載的。
- LRecyclerView是如何在RecyclerView基礎(chǔ)上加上footer和header的
我們知道listview原生支持footer和header,如果我們看過listview的源碼的話,就知道他們是在通過adapter實(shí)現(xiàn)的,listView在添加header時(shí)代碼如下:
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (mAdapter != null) {
//如果是設(shè)置header,那么通過HeaderViewListAdapter的代理wrapperadapter來包裝真正的adapter
if (!(mAdapter instanceof HeaderViewListAdapter)) {
wrapHeaderListAdapterInternal();
}
// In the case of re-adding a header view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
當(dāng)添加header時(shí),將mAdapter通過方法wrapHeaderListAdapterInternal()包裝,HeaderViewListAdapter是mAdapter的代理類,可以看到類內(nèi)部有成員變量mAdapter,就是ListView的使用者真實(shí)創(chuàng)建的adapter。
通過以下代碼我們就一目了然他的實(shí)現(xiàn)原理了:實(shí)現(xiàn)原理請參考注釋。
public View getView(int position, View convertView, ViewGroup parent) {
// Header (negative positions will throw an IndexOutOfBoundsException)
int numHeaders = getHeadersCount();
//如果是position指向header,那么從mHeaderViewInfos返回對應(yīng)view
if (position < numHeaders) {
return mHeaderViewInfos.get(position).view;
}
// Adapter
final int adjPosition = position - numHeaders;
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
//如果是position指向mAdapter實(shí)際列表數(shù)據(jù),那么調(diào)用mAdapter.getView
if (adjPosition < adapterCount) {
return mAdapter.getView(adjPosition, convertView, parent);
}
}
//如果是position指向footer,那么從mFooterViewInfos返回對應(yīng)view
// Footer (off-limits positions will throw an IndexOutOfBoundsException)
return mFooterViewInfos.get(adjPosition - adapterCount).view;
}
同時(shí)getCount getItemType getItem等實(shí)現(xiàn)均對 footer和header進(jìn)行了考慮,這樣包裝類封裝了mAdapter本身和 footer header,將他們作為一個(gè)整體提供給listview。
本控件的作者借鑒了這個(gè)思路,設(shè)計(jì)了代理類LRecyclerViewAdapter,類里類似的也含有mInnerAdapter實(shí)際的adapter,mHeaderViews和mFooterViews則用于保存信息。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//分別RefreshHeader header footer三種類型返回不同的ViewHolder
//這里RefreshHeader沒有像PullRefreshView一樣作為listview之外的view存在,而是放入
//adapter內(nèi)部讓listview(RecyclerView)一起加載。
//如何雖手勢控制RefreshHeader的Layout,后面詳細(xì)說。
if (viewType == TYPE_REFRESH_HEADER) {
return new ViewHolder(mRefreshHeader.getHeaderView());
} else if (isHeaderType(viewType)) {
return new ViewHolder(getHeaderViewByType(viewType));
} else if (viewType == TYPE_FOOTER_VIEW) {
return new ViewHolder(mFooterViews.get(0));
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
和listview的HeaderViewListAdapter一樣,LRecyclerViewAdapter也是類似的處理:
@Override
public int getItemCount() {
if (mInnerAdapter != null) {
//此處+1,是考慮到RefreshHeader,就是說header和RefreshHeader是不同的功能,可能同時(shí)出現(xiàn)
//而footer作為一般的footer或者上拉加載的footer,只會出現(xiàn)一種
return getHeaderViewsCount() + getFooterViewsCount() + mInnerAdapter.getItemCount() + 1;
} else {
return getHeaderViewsCount() + getFooterViewsCount() + 1;
}
}
在閱讀以上代碼時(shí),大家不免會有個(gè)疑問,LRecyclerView的使用上并不像listview那樣簡練,LRecyclerView在設(shè)置adapter時(shí),需要手動創(chuàng)建innerAdapter和wrapperadapter,將innerAdapter包裹進(jìn)WrapperAdapter后設(shè)置給LRecyclerView;而listview會根據(jù)header/footer使用情況自動創(chuàng)建wrapperadapter,使用者并不知道代理類的存在。此處的設(shè)計(jì)在文章的最后會闡述我的一些看法。
- LRecyclerView是如何實(shí)現(xiàn)下拉刷新和上拉分頁的
如何下拉刷新:LRecyclerView下拉刷新也是是通過onInterceptTouchEvent和onTouchEvent來實(shí)現(xiàn)的,具體的實(shí)現(xiàn)和PullRefreshView類似,此處不單獨(dú)分析了。通過接口IRefreshHeader來控制RefreshHeader的狀態(tài)改變。刷新后通過OnRefreshListener接口通知業(yè)務(wù)刷新數(shù)據(jù)。
如何分頁加載:利用RecyclerView的onScrolled回調(diào),控件滑動過程中不斷回調(diào)此方法,通過判斷是否滑動到最底部來決定是否上拉加載,代碼如下:
if (mLoadMoreListener != null && mLoadMoreEnabled) {
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
if (visibleItemCount > 0
&& lastVisibleItemPosition >= totalItemCount - 1
&& totalItemCount > visibleItemCount
&& !isNoMore
&& !mRefreshing) {
mFootView.setVisibility(View.VISIBLE);
if (!mLoadingData) {
mLoadingData = true;
//更新footerView的狀態(tài)
mLoadMoreFooter.onLoading();
if (mWrapAdapter != null) {
//回調(diào)業(yè)務(wù) 分頁加載更多
mWrapAdapter.loadMore(mLoadMoreListener);
}
}
}
}
2、易用性:
此控件通過將IRefreshHeader和ILoadMoreFooter兩個(gè)接口的拆分,相比較PullRefreshView對于上拉footer的處理更加直接和便捷。ILoadMoreFooter的不同接口更加適應(yīng)于分頁加載的不同狀態(tài)。并且不同狀態(tài)的文案是可以定制的:public void setFooterViewHint(String loading, String noMore, String noNetWork)
這樣對于上拉分頁的情況,不需要業(yè)務(wù)再對控件做二次開發(fā)(PullRefreshView需要),是更加易用的。
但是業(yè)務(wù)上對于分頁加載需求的邏輯負(fù)擔(dān)還是比較大,集中在以下兩點(diǎn)(PullRefreshView也存在此問題)
1)分頁pageNumber pageSize等需要業(yè)務(wù)維護(hù),而這些邏輯都是通用的。
2)判斷是否需要加載更多,還是沒有更多數(shù)據(jù),的邏輯業(yè)務(wù)需要維護(hù),也是可以通用的。
這兩個(gè)問題其實(shí)都可以在wrapperAdapter中通過統(tǒng)一的邏輯來處理,只不過業(yè)務(wù)加載后要通知控件:除了LRecyclerView在加載更多時(shí)通知業(yè)務(wù)onLoadMore,業(yè)務(wù)在加載更多后也要通過接口ILoadCallback把結(jié)果傳入wrapperAdapter中,這樣wrapperAdapter便可以在回調(diào)后根據(jù)當(dāng)前adapter內(nèi)的數(shù)據(jù)統(tǒng)一處理pageNumer等字段,維護(hù)是否加載更多的狀態(tài)了。
我們自定義的ILoadCallback接口,業(yè)務(wù)在onLoadMore處理完后,要根據(jù)返回的結(jié)果調(diào)用的接口。
public interface ILoadCallback {
//業(yè)務(wù)loadMore的結(jié)果 success和failue都通知wrapperAdapter
//wrapperAdapter通過innerAdapter的數(shù)據(jù)就可以處理了
void onSuccess();
void onFailure();
}
WrapperAdapter對接口調(diào)用的處理:維護(hù)pageNumber,和footer是否加載更多等狀態(tài)
private ILoadCallback mLoadCallback = new ILoadCallback() {
@Override
public void onSuccess() {
notifyDataSetChanged();
if ((mInnerAdapter.getItemCount() % getItemNumInPage()) == 0){
//判斷還需要加載下一頁
mCurrentPage++;
if (mLRecyclerView != null) {
mLRecyclerView.setNoMore(false);
}
} else {
//判斷沒有更多數(shù)據(jù),并將footerview設(shè)置為noMore
if (mLRecyclerView != null) {
mLRecyclerView.setNoMore(true);
}
}
if (mLRecyclerView != null) {
mLRecyclerView.refreshComplete(getItemNumInPage());
}
}
@Override
public void onFailure() {
//失敗時(shí)統(tǒng)一提示,并集成再次點(diǎn)擊,多加載一次的功能
mLRecyclerView.refreshComplete(getItemNumInPage());
mLRecyclerView.setOnNetWorkErrorListener(new OnNetWorkErrorListener() {
@Override
public void reload() {
if (mLoadMoreCallback != null) {
mLoadMoreCallback.onLoadMore(mCurrentPage, getItemNumInPage(), mLoadCallback);
}
}
});
}
};
經(jīng)過這樣進(jìn)一步的封裝,LRecyclerView的使用易用性進(jìn)一步提升了。可以說比PullRefreshView本身的易用性要強(qiáng)一些,尤其是在分頁加載的邏輯封裝方面
3、擴(kuò)展性:
PullRefreshView自身支持所有ViewGroup的下拉刷新。我覺得LRecyclerView與PullRefreshView相比,在架構(gòu)上犧牲了一些擴(kuò)展性,但易用性有很大的提升,應(yīng)用場景有較強(qiáng)的針對性。而擴(kuò)展性方面,利用Recyclerview自身很強(qiáng)的擴(kuò)展性,就可以應(yīng)付大部分使用場景。當(dāng)然header footer RefreshHeader這些樣式的擴(kuò)展是自然支持的。
4、穩(wěn)定性:
github star數(shù)在2000以上,issue修改及時(shí),在二次開發(fā)的過程中,上拉分頁的footer狀態(tài)維護(hù)有些小bug,但是基本不影響穩(wěn)定性,產(chǎn)品上線后控件的崩潰率一直很低。基本可以放心使用。
5、其他的思考:
wrapperAdapter的設(shè)置:
上文中提及過的,WrapperAdapter和innerAdapter都需要在業(yè)務(wù)上的新建有點(diǎn)雞肋(因?yàn)榭梢栽贚RecyclerView setAdatper時(shí),內(nèi)部創(chuàng)建wrapperAdapter,和listview的做法一致),作者這么做的原因,我想可能是WrapperAdapter承載了很多框架業(yè)務(wù)的功能,那么業(yè)務(wù)持有此變量可以非常方便的調(diào)用WrapperAdapter的接口。在我看來,較為合理的方式還是將WrapperAdapter不對外暴露,將原來WrapperAdapter的對外接口改到LRecyclerView來實(shí)現(xiàn)。這樣用戶調(diào)用方便,同時(shí)對控件的封裝性更好。
此封裝方案我在demo project中試驗(yàn)過,沒有太大問題,可能有些細(xì)節(jié)需要處理,后續(xù)我們的控件二次開發(fā)會采用這種方式。