一篇文章帶你擼遍下拉重新整理 分頁載入控制元件

一篇文章擼遍下拉重新整理 分頁載入控制元件
本文的研究物件是,在實際開發中經常用到的下拉重新整理和分頁載入功能。這兩個功能往往相伴相生,下拉重新整理是基於互動體驗上的功能,已經是普遍工人的移動端的資料重新整理互動(不限於列表);分頁載入一般考慮到後臺資料的分頁請求,降低後臺的壓力和網路延遲。 有沒有將二者結合的比較好的第三方控制元件呢,本文將針對主流github三方控制元件,帶你一一解讀。
主流下拉重新整理控制元件橫評
備註:我將從實現原理、易用性、擴充套件性、穩定性四個方面比較 易用性:包括 1、使用是否方便,xml java均可配置使用 2、是否將常用的邏輯功能封裝(分頁計算、footer顯示與否等),使用者不關心細節 3、對一些常用的擴充套件是否已支援可配置(如header的自定義樣式等) 擴充套件性:包括 1、支援的下拉、分頁的ViewGroup是否可方便擴充套件 2、header footer等是否擴充套件方便 穩定性:包括 1、github活躍性,issue是否及時處理 2、上線後控制元件內部crash
一、最早的先行者:XListView
( Android%25EF%25BC%2589" rel="nofollow,noindex">github.com/Maxwin-z/XL…
1、實現原理:
XListView直接extends ListView,使用也和Listview一樣,header和footer也是採用ListView自帶的功能,僅對二者的layout做了封裝XListViewFooter和XListViewHeader。 從程式碼結構來看,非常簡單。header和footer的顯示與否,通過listview的onTouchEvent來判斷。

2、易用性:
與ListView同,但是下拉和分頁的可配置性幾乎沒有,常用封裝全無
3、擴充套件性:
很差,只能在使用ListView時使用,擴充套件需要改動程式碼,程式碼本身擴充套件性考慮很少。
4、穩定性:
github已停更,有些線上經典crash難於解決。
- 作為最早Android下拉重新整理功能的實踐者,僅有有歷史意義
二、廣泛應用者:PullToRefresh
1、實現原理:
其類圖可以較好的說明,其架構方式:

PullToRefresh基本奠定了經典下拉重新整理控制元件的架構形式:
- 1)一部分是下拉和分頁的骨架:核心content的載入和擴充套件、footer和header的載入、state的切換
- 2)一部分是footer和header的處理:footer header的互動、定製和擴充套件基於state。 依據以上兩部分,基於IPullToRefresh和 ILoadingLayout兩個介面開發。
- 核心骨架
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新增到其中。
- 擴充套件方式: abstract方法createRefreshableView(),在子類中實現用於擴充套件contentView footer header的擴充套件通過createLoadingLayout()返回,只要繼承自LoadingLayout即可擴充套件。當然控制元件本身提供了集中常用的Loadinglayout(FlipLoadingLayout RotateLoadingLayout)
- 互動處理: 如何從手勢的變化決定header以及footer的state呢?是通過onInterceptTouchEvent和OnTouchEvent。 和其他的touch事件處理類似,onInterceptTouchEvent方法作為前置準備,onTouchEvent方法實際處理手勢操作
@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來區分上拉和下拉,其實上拉和下拉的邏輯,從整體上是可以歸一的,有幾個關鍵點
- 1、判斷上拉 下拉的邏輯閾值:isReadyForPullStart()isReadyForPullEnd()分別是下拉 上拉的閾值方法,子類需要根據 mRefreshableView來實現
- 2、在不同的state下做不同的處理: 兩者都有 reset PULL_TO_REFRESH RELEASE_TO_REFRESH REFRESHING等狀態,上拉不需要區分PULL_TO_REFRESH RELEASE_TO_REFRESH兩種state而已。所以既然都是基於一套state的處理方案,那麼根據手勢滑動方向決定當前mCurrentMode,進而交給header 或 footer來處理state就是可行的。
- footer和header的擴充套件和處理
剛才說到了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(); 複製程式碼
2、易用性:
- 1、使用是否方便,xml和java程式碼都可以初始化和配置控制元件,這是控制元件設計初期就考慮到的
- 2、我們知道為了保證擴充套件性,架構上的實現不能過於具體,否則靈活性降低。架構上基於介面和抽象類進行設計,能保證在整體架構內部方便擴充套件。同時也提供了一些常用的具體實現類,比如PullToRefreshListView FlipLoadingLayout。
- 3、一些業務上的常用邏輯:(分頁計算、footer多個狀態的顯示等)沒有整合,需要二次開發
3、擴充套件性:
- mRefreshableView的設計理念,可以說讓控制元件理論上可以支援任何檢視類(ViewGroup)的下拉重新整理操作,比如後期擴充套件RecyclerView、ViewPager等。
- 從類圖中可以看出 PullToRefreshBase的多層子類,設計合理,層次分明。二次開發中可以選擇合適的基類進行擴充套件。
- LoadingLayoutProxy機制的引入,為實現更多LoadingLayout的state流轉提供了可能。
- 模板方法設計模式,基於介面開發,abstract基類,易於擴充套件和維護
4、穩定性:
github star 8700多,多個工程中考驗,類庫內部崩潰率較低。
三、官方控制元件:SwipeRefreshLayout
一兩句就能說清:
這個控制元件作為targetView(比如listview)的parentView出現,而且SwipeRefreshLayout只能有一個childView。 互動上比較單一,materialDesign風格,loading圖示在targetView之上顯示,targetView本身可以是任何view,擴充套件性沒的說。
四、基於RecyclerView的控制元件:LRecyclerView
LRecyclerView是csdn大牛‘一葉飄舟’所著,設計的初衷是為了打造一個更為好用的RecyclerView,一切基於RecyclerView架構搭建。
- 增加了header footer功能(不同於listview,為了擴充套件性,原生的RecyclerView並不支援header和footer)。
- 增加了下拉重新整理和上拉分頁載入功能(這個功能後來被更廣泛使用,所以在已有架構上支援了PullScrollView、PullWebView)。最終達到了現有的面貌。
- 目前我們已經將RecyclerView作為開發的主力控制元件,那麼基於RecyclerView的一個易用性、擴充套件性和穩定性各方面都均衡的控制元件,就是我們研究的目標。
1、實現原理:
有了以上的背景,我們對LRecyclerView這個控制元件會有一個大概認識。我們看下程式碼分佈:

從他的程式碼分佈可以看出,基本是圍繞LRecyclerview開展的。類之間的相互關係比較簡單,就不用類圖展開了。
以下我們將從兩個方面分析實現原理
- 1、LRecyclerView是如何在RecyclerView基礎上加上footer和header;
- 2、LRecyclerView是如何實現下拉重新整理和上拉分頁載入的。
- LRecyclerView是如何在RecyclerView基礎上加上footer和header的: 我們知道listview原生支援footer和header,如果我們看過listview的原始碼的話,就知道他們是在通過adapter實現的,listView在新增header時程式碼如下:
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,使用者並不知道代理類的存在。此處的設計在文章的最後會闡述我的一些看法。
- LRecyclerView是如何實現下拉重新整理和上拉分頁的
- 如何下拉重新整理:LRecyclerView下拉重新整理也是是通過onInterceptTouchEvent和onTouchEvent來實現的,具體的實現和PullRefreshView類似,此處不單獨分析了。通過介面IRefreshHeader來控制RefreshHeader的狀態改變。重新整理後通過OnRefreshListener介面通知業務重新整理資料。
- 如何分頁載入:利用RecyclerView的onScrolled回撥,控制元件滑動過程中不斷回撥此方法,通過判斷是否滑動到最底部來決定是否上拉載入,程式碼如下:
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); } } } } 複製程式碼
2、易用性:
此控制元件將IRefreshHeader和ILoadMoreFooter兩個介面拆分,相比較PullRefreshView對於上拉footer的處理更加直接和便捷。兩個不同介面更加適應於分頁載入的不同狀態。並且不同狀態的文案是可以定製的:
public void setFooterViewHint(String loading, String noMore, String noNetWork)
這樣對於上拉分頁的情況,不需要業務再對控制元件做二次開發(PullRefreshView需要),是更加易用的。 但是業務上對於分頁載入需求的邏輯負擔還是比較大,集中在以下兩點
- 1)分頁pageNumber pageSize等需要業務維護,而這些邏輯都是通用的。
- 2)判斷是否需要載入更多,還是沒有更多資料,的邏輯業務需要維護,這些邏輯也是通用的。
基於此,我們針對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本身的易用性要強一些,尤其是在分頁載入的邏輯封裝方面
3、擴充套件性:
PullRefreshView自身支援所有ViewGroup的下拉重新整理。我覺得LRecyclerView與PullRefreshView相比,在架構上犧牲了一些擴充套件性,但易用性有很大的提升,應用場景有較強的針對性。實際使用中,利用Recyclerview自身很強的擴充套件性,就可以應付大部分使用場景。
4、穩定性:
github star數在2000以上,issue修改及時,在二次開發的過程中,上拉分頁的footer狀態維護有些小bug,但是基本不影響穩定性,產品上線後控制元件的崩潰率一直很低。基本可以放心使用。
5、其他的思考:
wrapperAdapter的設定: 文中提及過的,WrapperAdapter和innerAdapter都需要在業務上新建有點雞肋(因為可以在LRecyclerView setAdatper時,內部建立wrapperAdapter,和listview的做法一致),作者這麼做的原因,我想可能是WrapperAdapter承載了很多框架業務的功能,那麼業務持有此變數可以非常方便的呼叫WrapperAdapter的介面。在我看來,較為合理的方式還是將WrapperAdapter不對外暴露,將原來WrapperAdapter的對外介面改到LRecyclerView來實現。這樣使用者呼叫方便,同時對控制元件的封裝性更好。 此封裝方案我在demo project中試驗過,沒有太大問題,可能有些細節需要處理,後續我們的控制元件二次開發會採用這種方式。
總結
在我們自己的專案演進過程中,經歷從xlistview到PullRefreshView到LRecyclerView的轉變,所以對各自控制元件的優點、劣勢,適用範圍都比較清楚。之所以最終將LRecyclerView最為主力控制元件,除了文中提到的原因以外,還有比較關鍵的一點:在分頁載入的二次開發中,LRecyclerView給予了足夠的擴充套件性,也為今後我們功能的拓展提供了足夠的信心。