20分鐘完全掌握Android下拉刷新控件

下拉刷新分頁加載控件分析


主流下拉刷新控件橫評

備注:我將從實(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ā)。

  1. 核心骨架
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就是可行的。

  1. 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)下拉刷新和上拉分頁加載的。

  1. 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ì)在文章的最后會闡述我的一些看法。

  1. 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ā)會采用這種方式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,486評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內(nèi)容

  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,811評論 2 59
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,718評論 25 708
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,862評論 22 665
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 32,954評論 6 472
  • 所謂掙扎,就是當(dāng)下的我、過去的我和理想中的我之間角色的切換。有時(shí)候能夠在一段時(shí)間內(nèi)持續(xù)做出過去做不成的事情,有時(shí)候...
    陳玲敏Alita閱讀 253評論 0 2