一篇文章带你撸遍下拉刷新 分页加载控件

栏目: IOS · Android · 发布时间: 5年前

内容简介:本文的研究对象是,在实际开发中经常用到的下拉刷新和分页加载功能。这两个功能往往相伴相生,下拉刷新是基于交互体验上的功能,已经是普遍工人的移动端的数据刷新交互(不限于列表);分页加载一般考虑到后台数据的分页请求,降低后台的压力和网络延迟。 有没有将二者结合的比较好的第三方控件呢,本文将针对主流github三方控件,带你一一解读。备注:我将从实现原理、易用性、扩展性、稳定性四个方面比较 易用性:包括 1、使用是否方便,xml java均可配置使用 2、是否将常用的逻辑功能封装(分页计算、footer显示与否等
一篇文章带你撸遍下拉刷新 分页加载控件

一篇文章撸遍下拉刷新 分页加载控件

本文的研究对象是,在实际开发中经常用到的下拉刷新和分页加载功能。这两个功能往往相伴相生,下拉刷新是基于交互体验上的功能,已经是普遍工人的移动端的数据刷新交互(不限于列表);分页加载一般考虑到后台数据的分页请求,降低后台的压力和网络延迟。 有没有将二者结合的比较好的第三方控件呢,本文将针对主流github三方控件,带你一一解读。

主流下拉刷新控件横评

备注:我将从实现原理、易用性、扩展性、稳定性四个方面比较 易用性:包括 1、使用是否方便,xml java均可配置使用 2、是否将常用的逻辑功能封装(分页计算、footer显示与否等),使用者不关心细节 3、对一些常用的扩展是否已支持可配置(如header的自定义样式等) 扩展性:包括 1、支持的下拉、分页的ViewGroup是否可方便扩展 2、header footer等是否扩展方便 稳定性:包括 1、github活跃性,issue是否及时处理 2、上线后控件内部crash

一、最早的先行者:XListView

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

( github.com/chrisbanes/… )

1、实现原理:

其类图可以较好的说明,其架构方式:

一篇文章带你撸遍下拉刷新 分页加载控件

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添加到其中。

  • 扩展方式: 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就是可行的。
  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();
复制代码

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是如何实现下拉刷新和上拉分页加载的。
  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);
            }
        }
    }

}
复制代码

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给予了足够的扩展性,也为今后我们功能的拓展提供了足够的信心。


以上所述就是小编给大家介绍的《一篇文章带你撸遍下拉刷新 分页加载控件》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Development Recipes

Web Development Recipes

Brian P. Hogan、Chris Warren、Mike Weber、Chris Johnson、Aaron Godin / Pragmatic Bookshelf / 2012-1-22 / USD 35.00

You'll see a full spectrum of cutting-edge web development techniques, from UI and eye candy recipes to solutions for data analysis, testing, and web hosting. Make buttons and content stand out with s......一起来看看 《Web Development Recipes》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具