本文的研究對象是,在實際開發中常常用到的下拉刷新和分頁加載功能。這兩個功能每每相伴相生,下拉刷新是基於交互體驗上的功能,已是廣泛工人的移動端的數據刷新交互(不限於列表);分頁加載通常考慮到後臺數據的分頁請求,下降後臺的壓力和網絡延遲。 有沒有將兩者結合的比較好的第三方控件呢,本文將針對主流github三方控件,帶你一一解讀。java
備註:我將從實現原理、易用性、擴展性、穩定性四個方面比較 易用性:包括 一、使用是否方便,xml java都可配置使用 二、是否將經常使用的邏輯功能封裝(分頁計算、footer顯示與否等),使用者不關心細節 三、對一些經常使用的擴展是否已支持可配置(如header的自定義樣式等) 擴展性:包括 一、支持的下拉、分頁的ViewGroup是否可方便擴展 二、header footer等是否擴展方便 穩定性:包括 一、github活躍性,issue是否及時處理 二、上線後控件內部crashgit
(github.com/Maxwin-z/XL…github
XListView直接extends ListView,使用也和Listview同樣,header和footer也是採用ListView自帶的功能,僅對兩者的layout作了封裝XListViewFooter和XListViewHeader。 從代碼結構來看,很是簡單。header和footer的顯示與否,經過listview的onTouchEvent來判斷。 設計模式
與ListView同,可是下拉和分頁的可配置性幾乎沒有,經常使用封裝全無bash
不好,只能在使用ListView時使用,擴展須要改動代碼,代碼自己擴展性考慮不多。網絡
github已停更,有些線上經典crash難於解決。架構
其類圖能夠較好的說明,其架構方式: 框架
private void init(Context context, AttributeSet attrs) {
setGravity(Gravity.CENTER);
ViewConfiguration config = ViewConfiguration.get(context);
mTouchSlop = config.getScaledTouchSlop();
....//Parse styleable
// Refreshable View 用於擴展
// 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方法構造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添加到其中。ide
@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狀態的變化 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)) { //拉動結束,在RELEASE_TO_REFRESH狀態下鬆手,變爲REFRESHING setState(State.REFRESHING, true); return true; } // If we're already refreshing, just scroll back to the top
if (isRefreshing()) {
//拉動結束,在REFRESHING狀態下鬆手,回到原點
smoothScrollTo(0);
return true;
}
// If we haven't returned by here, then we're not in a state
// to pull, so just reset
//拉動結束,在其餘狀態(PULL_TO_REFRESH)下鬆手,reset到初始狀態
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);//根據滑動的位置更新footerLayout
break;
case PULL_FROM_START://下拉刷新
default:
mHeaderLayout.onPull(scale);//根據滑動的位置更新headerLayout
break;
}
//根據滑動的位置(是否超過閾值),決定狀態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);
}
}
}
複製代碼
從以上代碼容易理解下拉刷新的邏輯脈絡,可是上拉分頁加載是怎麼實現的呢? PullToRefreshBase控件經過mCurrentMode來區分上拉和下拉,其實上拉和下拉的邏輯,從總體上是能夠歸一的,有幾個關鍵點
剛纔說到了footer和header是在同一套state狀態下的處理機制,其回調也相似。因此二者繼承同一接口和基類。PullToRefreshBase控件採用了Proxy的方式,實現了兩者的統一調用。 也就是說LoadingLayoutProxy 、headerLoadingLayout、footerLoadingLayout均實現ILoadingLayout,LoadingLayoutProxy是headerLoadingLayout與footerLoadingLayout兩者的代理,在state的流轉過程當中,經過LoadingLayoutProxy的調用,達到header 和footer兩個loadingLayout的同步調用。 LoadingLayout基類已經實現了基本的layout,咱們本身定製的子類(例如CustomLoadingLayout),對裏面的動畫,文案等進行定製便可,基於ILoadingLayout接口徹底重寫一個新的,目前看不行,一方面PullToRefreshBase控件內部不少地方強轉到LoadingLayout。並且LoadingLayout基類(abstract類)預留了stated的回調抽象方法,供子類實現:
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();
複製代碼
github star 8700多,多個工程中考驗,類庫內部崩潰率較低。
這個控件做爲targetView(好比listview)的parentView出現,並且SwipeRefreshLayout只能有一個childView。 交互上比較單一,materialDesign風格,loading圖標在targetView之上顯示,targetView自己能夠是任何view,擴展性沒的說。
LRecyclerView是csdn大牛‘一葉飄舟’所著,設計的初衷是爲了打造一個更爲好用的RecyclerView,一切基於RecyclerView架構搭建。
有了以上的背景,咱們對LRecyclerView這個控件會有一個大概認識。咱們看下代碼分佈:
如下咱們將從兩個方面分析實現原理
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (mAdapter != null) {
//若是是設置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();
}
}
}
複製代碼
當添加header時,將mAdapter經過方法wrapHeaderListAdapterInternal()包裝,HeaderViewListAdapter是mAdapter的代理類,能夠看到類內部有成員變量mAdapter,就是ListView的使用者真實建立的adapter。 經過如下代碼咱們就一目瞭然他的實現原理了:實現原理請參考註釋。
public View getView(int position, View convertView, ViewGroup parent) {
// Header (negative positions will throw an IndexOutOfBoundsException)
int numHeaders = getHeadersCount();
//若是是position指向header,那麼從mHeaderViewInfos返回對應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實際列表數據,那麼調用mAdapter.getView
if (adjPosition < adapterCount) {
return mAdapter.getView(adjPosition, convertView, parent);
}
}
//若是是position指向footer,那麼從mFooterViewInfos返回對應view
// Footer (off-limits positions will throw an IndexOutOfBoundsException)
return mFooterViewInfos.get(adjPosition - adapterCount).view;
}
複製代碼
同時getCount getItemType getItem等實現均對 footer和header進行了考慮,這樣包裝類封裝了mAdapter自己和 footer header,將他們做爲一個總體提供給listview。 本控件的做者借鑑了這個思路,設計了代理類LRecyclerViewAdapter,類裏相似的也含有mInnerAdapter實際的adapter,mHeaderViews和mFooterViews則用於保存信息。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//分別RefreshHeader header footer三種類型返回不一樣的ViewHolder
//這裏RefreshHeader沒有像PullRefreshView同樣做爲listview以外的view存在,而是放入
//adapter內部讓listview(RecyclerView)一塊兒加載。
//如何雖手勢控制RefreshHeader的Layout,後面詳細說。
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是不一樣的功能,可能同時出現
//而footer做爲通常的footer或者上拉加載的footer,只會出現一種
return getHeaderViewsCount() + getFooterViewsCount() + mInnerAdapter.getItemCount() + 1;
} else {
return getHeaderViewsCount() + getFooterViewsCount() + 1;
}
}
複製代碼
在閱讀以上代碼時,你們難免會有個疑問,LRecyclerView的使用上並不像listview那樣簡練,LRecyclerView在設置adapter時,須要手動建立innerAdapter和wrapperadapter,將innerAdapter包裹進WrapperAdapter後設置給LRecyclerView;反觀listview會根據header/footer使用狀況自動建立wrapperadapter,使用者並不知道代理類的存在。此處的設計在文章的最後會闡述個人一些見解。
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的狀態
mLoadMoreFooter.onLoading();
if (mWrapAdapter != null) {
//回調業務 分頁加載更多
mWrapAdapter.loadMore(mLoadMoreListener);
}
}
}
}
複製代碼
此控件將IRefreshHeader和ILoadMoreFooter兩個接口拆分,相比較PullRefreshView對於上拉footer的處理更加直接和便捷。兩個不一樣接口更加適應於分頁加載的不一樣狀態。而且不一樣狀態的文案是能夠定製的:
public void setFooterViewHint(String loading, String noMore, String noNetWork)
這樣對於上拉分頁的狀況,不須要業務再對控件作二次開發(PullRefreshView須要),是更加易用的。 可是業務上對於分頁加載需求的邏輯負擔仍是比較大,集中在如下兩點
基於此,咱們針對LRecyclerView的分頁加載功能作了二次封裝。這兩個問題均可以在wrapperAdapter中經過統一的邏輯來處理,只不過業務加載後要要經過接口ILoadCallback通知控件:
咱們自定義的ILoadCallback接口,業務在onLoadMore處理完後,要根據返回的結果調用的接口。
public interface ILoadCallback {
//業務loadMore的結果 success和failue都通知wrapperAdapter
void onSuccess();
void onFailure();
}
複製代碼
WrapperAdapter對接口調用的處理:維護pageNumber,和footer是否加載更多等狀態 此前這些邏輯都須要重複寫在業務代碼中。
private ILoadCallback mLoadCallback = new ILoadCallback() {
@Override
public void onSuccess() {
notifyDataSetChanged();
if ((mInnerAdapter.getItemCount() % getItemNumInPage()) == 0){
//判斷還須要加載下一頁
mCurrentPage++;
if (mLRecyclerView != null) {
mLRecyclerView.setNoMore(false);
}
} else {
//判斷沒有更多數據,並將footerview設置爲noMore
if (mLRecyclerView != null) {
mLRecyclerView.setNoMore(true);
}
}
if (mLRecyclerView != null) {
mLRecyclerView.refreshComplete(getItemNumInPage());
}
}
@Override
public void onFailure() {
//失敗時統一提示,並集成再次點擊,多加載一次的功能
mLRecyclerView.refreshComplete(getItemNumInPage());
mLRecyclerView.setOnNetWorkErrorListener(new OnNetWorkErrorListener() {
@Override
public void reload() {
if (mLoadMoreCallback != null) {
mLoadMoreCallback.onLoadMore(mCurrentPage, getItemNumInPage(), mLoadCallback);
}
}
});
}
};
複製代碼
通過這樣進一步的封裝,LRecyclerView的使用易用性進一步提高了。能夠說比PullRefreshView自己的易用性要強一些,尤爲是在分頁加載的邏輯封裝方面
PullRefreshView自身支持全部ViewGroup的下拉刷新。我以爲LRecyclerView與PullRefreshView相比,在架構上犧牲了一些擴展性,但易用性有很大的提高,應用場景有較強的針對性。實際使用中,利用Recyclerview自身很強的擴展性,就能夠應付大部分使用場景。
github star數在2000以上,issue修改及時,在二次開發的過程當中,上拉分頁的footer狀態維護有些小bug,可是基本不影響穩定性,產品上線後控件的崩潰率一直很低。基本能夠放心使用。
wrapperAdapter的設置: 文中說起過的,WrapperAdapter和innerAdapter都須要在業務上新建有點雞肋(由於能夠在LRecyclerView setAdatper時,內部建立wrapperAdapter,和listview的作法一致),做者這麼作的緣由,我想多是WrapperAdapter承載了不少框架業務的功能,那麼業務持有此變量能夠很是方便的調用WrapperAdapter的接口。在我看來,較爲合理的方式仍是將WrapperAdapter不對外暴露,將原來WrapperAdapter的對外接口改到LRecyclerView來實現。這樣用戶調用方便,同時對控件的封裝性更好。 此封裝方案我在demo project中試驗過,沒有太大問題,可能有些細節須要處理,後續咱們的控件二次開發會採用這種方式。
在咱們本身的項目演進過程當中,經歷從xlistview到PullRefreshView到LRecyclerView的轉變,因此對各自控件的優勢、劣勢,適用範圍都比較清楚。之因此最終將LRecyclerView最爲主力控件,除了文中提到的緣由之外,還有比較關鍵的一點:在分頁加載的二次開發中,LRecyclerView給予了足夠的擴展性,也爲從此咱們功能的拓展提供了足夠的信心。