一篇文章帶你擼遍下拉刷新 分頁加載控件

一篇文章擼遍下拉刷新 分頁加載控件

本文的研究對象是,在實際開發中常常用到的下拉刷新和分頁加載功能。這兩個功能每每相伴相生,下拉刷新是基於交互體驗上的功能,已是廣泛工人的移動端的數據刷新交互(不限於列表);分頁加載通常考慮到後臺數據的分頁請求,下降後臺的壓力和網絡延遲。 有沒有將兩者結合的比較好的第三方控件呢,本文將針對主流github三方控件,帶你一一解讀。java


主流下拉刷新控件橫評

備註:我將從實現原理、易用性、擴展性、穩定性四個方面比較 易用性:包括 一、使用是否方便,xml java都可配置使用 二、是否將經常使用的邏輯功能封裝(分頁計算、footer顯示與否等),使用者不關心細節 三、對一些經常使用的擴展是否已支持可配置(如header的自定義樣式等) 擴展性:包括 一、支持的下拉、分頁的ViewGroup是否可方便擴展 二、header footer等是否擴展方便 穩定性:包括 一、github活躍性,issue是否及時處理 二、上線後控件內部crashgit

1、最先的先行者:XListView

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難於解決。架構

  • 做爲最先Android下拉刷新功能的實踐者,僅有有歷史意義

2、普遍應用者:PullToRefresh

(github.com/chrisbanes/…)app

一、實現原理:

其類圖能夠較好的說明,其架構方式: 框架

PullToRefresh基本奠基了經典下拉刷新控件的架構形式:

  • 1)一部分是下拉和分頁的骨架:核心content的加載和擴展、footer和header的加載、state的切換
  • 2)一部分是footer和header的處理:footer header的交互、定製和擴展基於state。 依據以上兩部分,基於IPullToRefresh和 ILoadingLayout兩個接口開發。
  1. 核心骨架
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

  • 擴展方式: 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來區分上拉和下拉,其實上拉和下拉的邏輯,從總體上是能夠歸一的,有幾個關鍵點

  • 一、判斷上拉 下拉的邏輯閾值:isReadyForPullStart()isReadyForPullEnd()分別是下拉 上拉的閾值方法,子類須要根據 mRefreshableView來實現
  • 二、在不一樣的state下作不一樣的處理: 二者都有 reset PULL_TO_REFRESH RELEASE_TO_REFRESH REFRESHING等狀態,上拉不須要區分PULL_TO_REFRESH RELEASE_TO_REFRESH兩種state而已。因此既然都是基於一套state的處理方案,那麼根據手勢滑動方向決定當前mCurrentMode,進而交給header 或 footer來處理state就是可行的。
  1. 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();
複製代碼
二、易用性:
  • 一、使用是否方便,xml和java代碼均可以初始化和配置控件,這是控件設計初期就考慮到的
  • 二、咱們知道爲了保證擴展性,架構上的實現不能過於具體,不然靈活性下降。架構上基於接口和抽象類進行設計,能保證在總體架構內部方便擴展。同時也提供了一些經常使用的具體實現類,好比PullToRefreshListView FlipLoadingLayout。
  • 三、一些業務上的經常使用邏輯:(分頁計算、footer多個狀態的顯示等)沒有集成,須要二次開發
三、擴展性:
  • mRefreshableView的設計理念,能夠說讓控件理論上能夠支持任何視圖類(ViewGroup)的下拉刷新操做,好比後期擴展RecyclerView、ViewPager等。
  • 從類圖中能夠看出 PullToRefreshBase的多層子類,設計合理,井井有條。二次開發中能夠選擇合適的基類進行擴展。
  • LoadingLayoutProxy機制的引入,爲實現更多LoadingLayout的state流轉提供了可能。
  • 模板方法設計模式,基於接口開發,abstract基類,易於擴展和維護
四、穩定性:

github star 8700多,多個工程中考驗,類庫內部崩潰率較低。


3、官方控件:SwipeRefreshLayout

一兩句就能說清:

這個控件做爲targetView(好比listview)的parentView出現,並且SwipeRefreshLayout只能有一個childView。 交互上比較單一,materialDesign風格,loading圖標在targetView之上顯示,targetView自己能夠是任何view,擴展性沒的說。


4、基於RecyclerView的控件:LRecyclerView

LRecyclerView是csdn大牛‘一葉飄舟’所著,設計的初衷是爲了打造一個更爲好用的RecyclerView,一切基於RecyclerView架構搭建。

  • 增長了header footer功能(不一樣於listview,爲了擴展性,原生的RecyclerView並不支持header和footer)。
  • 增長了下拉刷新和上拉分頁加載功能(這個功能後來被更普遍使用,因此在已有架構上支持了PullScrollView、PullWebView)。最終達到了現有的面貌。
  • 目前咱們已經將RecyclerView做爲開發的主力控件,那麼基於RecyclerView的一個易用性、擴展性和穩定性各方面都均衡的控件,就是咱們研究的目標。
一、實現原理:

有了以上的背景,咱們對LRecyclerView這個控件會有一個大概認識。咱們看下代碼分佈:

從他的代碼分佈能夠看出,基本是圍繞LRecyclerview開展的。類之間的相互關係比較簡單,就不用類圖展開了。

如下咱們將從兩個方面分析實現原理

  • 一、LRecyclerView是如何在RecyclerView基礎上加上footer和header;
  • 二、LRecyclerView是如何實現下拉刷新和上拉分頁加載的。
  1. 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,使用者並不知道代理類的存在。此處的設計在文章的最後會闡述個人一些見解。

  1. 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);
            }
        }
    }

}
複製代碼
二、易用性:

此控件將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自己的易用性要強一些,尤爲是在分頁加載的邏輯封裝方面

三、擴展性:

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給予了足夠的擴展性,也爲從此咱們功能的拓展提供了足夠的信心。

相關文章
相關標籤/搜索