关于RecyclerView底部刷新实现的文章已经很多了,但大都只介绍了其基本原理和框架,对其中的很多细节没有交代,无法直接使用。本文会着重介绍RecyclerView底部刷新实现的一些细节处理。

1. 顶部刷新和底部刷新

顶部刷新和底部刷新都是列表中两种常见的交互方式。顶部刷新通常对应更新数据,更新的数据会替换掉当前数据。而底部刷新则对应获取更多的数据,更多的数据会添加在当前数据的后面。

顶部刷新和底部刷新在其他文章中更多的被称为下拉刷新和上拉加载更多,不过个人并不喜欢这样的称谓,每次提及上拉和下拉时都会感觉很困惑,需要思考一下上拉和下拉究竟对应哪个操作。所以这里将这两种操作称为顶部刷新和底部刷新。当然如果读者没有这个困扰,觉得很容易区分上拉和下拉,不烦还是延续这种称谓。

本文只会介绍底部刷新,对顶部刷新会在后面的文章中再介绍。

2. RecyclerView底部刷新的原理

RecyclerView底部刷新的原理很简单,就是在RecyclerView最底部添加一个表示加载中的View,然后监听RecyclerView的滑动事件,当RecyclerView滑动时,判断是否滑动到了RecyclerView的底部,也就是最后一个加载中的View是否可见,如果滑动到了RecyclerView底部,则执行底部刷新操作,获取更多数据。最后当获取更多数据完成后,更新RecyclerView的Adapter。

3. RecyclerView底部刷新的一般实现

根据上述RecyclerView底部刷新的实现原理,可以知道RecyclerView底部刷新实际上包含如下步骤。注意这里的步骤并不代表代码的书写顺序,它更多的表示的是代码执行的顺序。

  1. 为RecyclerView底部添加一个表示加载中的View
  2. 设置RecyclerView的滑动事件监听,在滑动过程中,根据底部View是否可见,决定是否执行底部刷新操作
  3. 执行底部刷新时,获取更多数据
  4. 获取完数据后,通知Adapter更新RecyclerView

现分别介绍这4个步骤的实现。

在这之前,先限定一个约束条件。我们知道在使用RecyclerView时都需要调用其setLayoutManager()方法设置其LayoutManager,在V7包实现了三种类型的LayoutManager,即LinearLayoutManager,GridLayoutManager和StaggeredGridLayoutManager。这三种类型的LayoutManager在实现底部刷新时会有一些细节上的差异。为了简化描述和方便理解,在这里介绍RecyclerView底部刷新的一般实现时,只考虑LinearLayoutManager,对其他两种类型有差异的地方会在后文单独说明。

3.1 为RecyclerView底部添加一个表示加载中的View

表示加载中的View

这个表示加载中的View通常会使用一个居中显示的ProgressBar来表示。其布局如下。

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="40dp"><ProgressBar
        style="?android:attr/progressBarStyle"android:layout_gravity="center"android:layout_width="30dp"android:layout_height="30dp"/></FrameLayout>

不过这并非是强制要求,其具体样式可以根据需要自由定义。

后文会将这个表示加载中的View称为底部刷新View(Bottom Refresh View)。

为RecyclerView添加底部刷新View

为RecyclerView添加底部刷新View一般是通过将底部刷新View作为RecyclerView的Item来实现的。
为此需要改写RecyclerView Adapter的以下几个方法。

  1. getItemCount
    RecyclerView Adapter的getItemCount方法返回的是item的数量,既然要将底部刷新View作为RecyclerView的Item添加到RecyclerView中,就需要在原有item数量基础上加1。

    例如:

    @Override
    public int getItemCount() {return mList.size() + 1;
    }
  2. getItemViewType
    RecyclerView Adapter的getItemViewType方法返回的是item的类型,为了将底部刷新View对应的item和其他item区分开,需要将底部刷新View作为一个单独的类型返回。

    例如:

    @Override
    public int getItemViewType(int position) {if (position < mList.size()) {return TYPE_NORMAL_ITEM;} else {return TYPE_BOTTOM_REFRESH_ITEM;}
    }
  3. onCreateViewHolder
    RecyclerView Adapter的onCreateViewHolder方法用来创建ViewHolder。这里首先需要为底部刷新View定义一个ViewHolder,然后根据item的类型来决定要创建哪个ViewHolder。

    例如:

    // 定义底部刷新View对应的ViewHolder
    private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {BottomViewHolder(View itemView) {super(itemView);}
    }
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {// 如果是底部刷新View,则加载底部刷新View布局,并创建底部刷新View对应的ViewHolderif (viewType == TYPE_BOTTOM_REFRESH_ITEM) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);return new BottomRefreshViewHolder(view);}// 如果是其他类型的View,则按照正常流程创建普通的ViewHolderelse {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_normal_item, parent, false);return new NormalViewHolder(view);}
    }
  4. onBindViewHolder
    RecyclerView Adapter的onBindViewHolder方法用来将ViewHolder和对应的数据绑定起来。由于底部刷新View并不需要绑定任何数据,所以这里不需要对底部刷新ViewHolder做特别的处理,只需要判断下是否是底部刷新ViewHolder就可以了。

    例如:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {if (!(holder instanceof BottomRefreshViewHolder)) {...}
    }

完成了上述步骤之后即为RecyclerView底部添加一个底部刷新View

3.2 滑动事件处理

设置滑动事件监听

RecyclerView提供了addOnScrollListener()方法来设置滑动事件监听,只需要将监听滑动事件的RecyclerView.OnScrollListener对象作为参数传递进去即可。

例如:

addOnScrollListener(onScrollListener);

onScrollStateChanged()和onScrolled()

RecyclerView.OnScrollListener是一个抽象类,它包含两个方法onScrollStateChanged()和onScrolled()。

onScrollStateChanged()方法会在每次滑动状态发生改变时调用。例如,由静止状态变为滑动状态,或者由滑动状态变为静止状态时,onScrollStateChanged()方法都会被调用。

onScrolled()方法会在RecyclerView滑动时被调用,即使手指离开了屏幕,只要RecyclerView仍然在滑动onScrolled()就会被不断调用。

理论上来说,我们既可以将判断底部View是否可见和执行底部刷新操作的过程放到onScrollStateChanged()方法中执行,也可以将其放到onScrolled()方法中执行。但放到不同方法中执行在用户体验上会产生一些不同。

如果将判断底部View是否可见和执行底部刷新操作的过程放到onScrollStateChanged()方法中执行,意味着是以一次滑动过程的最终状态来决定是否要执行底部刷新。如果在一次滑动过程中间,底部View已经可见,但是最终停下来的时候底部View是不可见的,那么将不会执行底部刷新操作。

如果将判断底部View是否可见和执行底部刷新操作的过程放到onScrolled()方法中执行,意味着只要在一次滑动过程中间底部View可见,那么将会立刻触发底部刷新操作。

观察大部分的APP,都是只要出现底部加载中View,就会开始执行底部刷新操作,这也和一般用户的认知相一致。所以,一般我们都会将判断底部View是否可见和执行底部刷新操作的过程放到onScrolled()方法中执行。但是onScrollStateChanged()方法仍然是有用的,有些辅助的逻辑会放到其中来执行。具体哪些逻辑需要放到onScrollStateChanged()方法中会在文章后面提到。

判断底部刷新View是否可见

判断底部刷新View是否可见是实现RecyclerView底部刷新功能的关键。不过幸好它的实现并不复杂。

在LinearLayoutManager中提供了一个方法可以获取到当前最后一个可见的item在RecyclerView Adapter中的位置,如果这个位置恰好等于RecyclerView Adapter中item的数量减1,那么就表示底部刷新View已经可见了。这也很容易理解,例如RecyclerView Adapter中有55个item,由于Adapter中的位置都是从0开始的,所以这55个item的位置就是从0到54,最后一个item(也就是底部刷新View对应的item)的位置是54。如果当前最后一个可见的item位置为54,那么就表示底部刷新View是可见的。

对LinearLayoutManager,可以调用其findLastVisibleItemPosition()方法来获取当前最后一个可见的item在RecyclerView Adapter中的位置。

示例代码如下。

private int getLastVisibleItemPosition() {RecyclerView.LayoutManager manager = getLayoutManager();if (manager instanceof LinearLayoutManager) {return ((LinearLayoutManager) manager).findLastVisibleItemPosition();}return NO_POSITION;
}private boolean isBottomViewVisible() {int lastVisibleItem = getLastVisibleItemPosition();return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;
}

执行底部刷新操作

将上述几个步骤组合在一起就可以得到完整的滑动事件处理过程。示例代码如下。

RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);if (isBottomViewVisible()) {requestMoreData();}}
};
addOnScrollListener(onScrollListener);

3.3 获取更多数据

获取数据的流程一般都是通过调用约定的接口从服务端获取数据,这属于业务逻辑,这里不做介绍了。

3.4 更新RecyclerView

获取数据一般都是异步过程,在获取数据完成后,调用RecyclerView Adapter的相关方法更新RecyclerView。由于是获取更多数据,所以一般可以调用notifyItemInserted()或者notifyItemRangeInserted()来更新RecyclerView。

至此,RecyclerView底部刷新的基本实现就已经完成了。

4. 底部刷新功能的封装

上述底部刷新功能的实现,包含了两部分的修改,一部分是对RecyclerView自身的一些设置,例如设置滑动事件监听,判断底部刷新View是否可见等。另外一部分是对RecyclerView Adapter的修改,也就是为RecyclerView添加底部刷新View。由于一个app中通常都会有多个界面需要实现底部刷新功能,如果每个要实现底部刷新功能的界面都这样实现一遍,实在是太麻烦,也会使原本的代码变得复杂和臃肿。因此,需要将上述底部刷新功能的实现封装在一起。

对第一部分RecyclerView自身的一些设置,可以很容易的通过继承RecyclerView来实现封装,然后在代码和xml中使用这个继承之后的RecyclerView即可。对第二部分RecyclerView Adapter的修改要麻烦一些,由于不同的列表都需要定义单独的Adapter,在这些Adapter中都需要重写getItemCount(),getItemViewType()这些方法。所以不能简单的通过继承RecyclerView Adapter,然后各个列表的Adapter再继承自这个修改后的Adapter来解决。为了实现Adapter的封装,需要实现一个内部的Adapter,然后用这个内部的Adapter包裹外部列表的Adapter来实现。

现分别对这两部分的封装过程进行介绍。

4.1 RecyclerView的封装

对RecyclerView的封装只需要实现一个类继承自RecyclerView,将底部刷新功能对RecyclerView的修改放到这个类中即可。

示例代码如下。

public class XRecyclerView extends RecyclerView {private OnBottomRefreshListener mBottomRefreshListener;private RecyclerView.OnScrollListener mOnScrollListener;private boolean mBottomRefreshable;public XRecyclerView(Context context) {super(context);init();}public XRecyclerView(Context context, AttributeSet attrs) {super(context, attrs);init();}private void init() {mBottomRefreshListener = null;mBottomRefreshable = false;mOnScrollListener = new RecyclerView.OnScrollListener() {@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);if (isBottomViewVisible()) {if (mBottomRefreshListener != null) {mBottomRefreshListener.onBottomRefresh();}}}};}private int getLastVisibleItemPosition() {RecyclerView.LayoutManager manager = getLayoutManager();if (manager instanceof LinearLayoutManager) {return ((LinearLayoutManager) manager).findLastVisibleItemPosition();}return NO_POSITION;}private boolean isBottomViewVisible() {int lastVisibleItem = getLastVisibleItemPosition();return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;}public boolean isBottomRefreshable() {return mBottomRefreshable;}// 设置底部下拉刷新监听public void setOnBottomRefreshListener(OnBottomRefreshListener listener) {mBottomRefreshListener = listener;if (mBottomRefreshListener != null) {addOnScrollListener(mOnScrollListener);mBottomRefreshable = true;} else {removeOnScrollListener(mOnScrollListener);mBottomRefreshable = false;}}public interface OnBottomRefreshListener {void onBottomRefresh();}
}

这里的代码和上面介绍RecyclerView底部刷新的一般实现时的示例代码基本一致。不同的是增加了一个OnBottomRefreshListener的接口类,setOnBottomRefreshListener()方法用来设置底部刷新事件的监听。当需要执行底部刷新时,调用OnBottomRefreshListener的onBottomRefresh()方法,通知外部更新数据。此外,将设置滑动事件监听也放到setOnBottomRefreshListener()方法中,只有设置了底部下拉刷新监听,才需要添加滑动事件监听,不然监听滑动事件是没有意义的。在setOnBottomRefreshListener()方法中还允许通过传入一个null对象,表示取消对底部下拉刷新的监听。mBottomRefreshable表示底部是否可以刷新,它会后面Adapter中用到。

4.2 RecyclerView Adapter的封装

对RecyclerView Adapter的封装需要在继承的RecyclerView类再实现一个包裹的Adapter(WrapperAdapter),并重新RecyclerView的setAdapter()方法,当在外部调用setAdapter()时会用WrapperAdapter包裹外部Adapter。

示例代码如下。

@Override
public void setAdapter(RecyclerView.Adapter adapter) {if (adapter != null) {WrapperAdapter wrapperAdapter = new WrapperAdapter(adapter);super.setAdapter(wrapperAdapter);}
}private class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;/*** 被包裹的外部Adapter*/private RecyclerView.Adapter mInnerAdapter;private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {@Overridepublic void onChanged() {super.onChanged();notifyDataSetChanged();}@Overridepublic void onItemRangeChanged(int positionStart, int itemCount) {super.onItemRangeChanged(positionStart, itemCount);notifyItemRangeChanged(positionStart, itemCount);}@Overridepublic void onItemRangeInserted(int positionStart, int itemCount) {super.onItemRangeInserted(positionStart, itemCount);notifyItemRangeInserted(positionStart, itemCount);}@Overridepublic void onItemRangeRemoved(int positionStart, int itemCount) {super.onItemRangeRemoved(positionStart, itemCount);notifyItemRangeRemoved(positionStart, itemCount);}@Overridepublic void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {super.onItemRangeMoved(fromPosition, toPosition, itemCount);notifyItemRangeChanged(fromPosition, toPosition + itemCount);}};private WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {if (mInnerAdapter != null) {notifyItemRangeRemoved(0, mInnerAdapter.getItemCount());mInnerAdapter.unregisterAdapterDataObserver(dataObserver);}this.mInnerAdapter = adapter;mInnerAdapter.registerAdapterDataObserver(dataObserver);notifyItemRangeInserted(0, mInnerAdapter.getItemCount());}public boolean isLoadMoreView(int position) {return isBottomRefreshable() && position == getItemCount() - 1;}@Overridepublic int getItemCount() {if (mInnerAdapter != null) {int itemCount = mInnerAdapter.getItemCount();if (isBottomRefreshable()) {return itemCount + 1;} else {return itemCount;}} else {return 0;}}@Overridepublic int getItemViewType(int position) {if (isBottomRefreshable()) {if (mInnerAdapter != null) {int adapterCount = mInnerAdapter.getItemCount();if (position < adapterCount) {return mInnerAdapter.getItemViewType(position);}}return TYPE_BOTTOM_REFRESH_ITEM;} else {if (mInnerAdapter != null) {return mInnerAdapter.getItemViewType(position);} else {return super.getItemViewType(position);}}}@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);return new BottomRefreshViewHolder(view);} else {return mInnerAdapter.onCreateViewHolder(parent, viewType);}}@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {if (!(holder instanceof BottomRefreshViewHolder)) {if (mInnerAdapter != null) {int adapterCount = mInnerAdapter.getItemCount();if (position <adapterCount) {mInnerAdapter.onBindViewHolder(holder, position);}}}}
}/*** bottom refresh View对应的ViewHolder*/
private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {BottomRefreshViewHolder(View itemView) {super(itemView);}
}

对这里的代码再稍作说明。

  1. 底部刷新View类型对应的整数值不能和外部Adapter已有的类型值重复,由于无法事先知道外部Adapter会定义哪些类型,所以这里为底部刷新View定义了一个相对来说比较特殊的数值Integer.MIN_VALUE + 1,但如果在实际使用时,发现外部Adapter也定义了这种类型,则需要修改这个数值,或者修改外部Adapter的定义。
  2. 由于外部数据更新时都是调用外部Adapter的notify…方法来通知RecyclerView更新,所以这里定义了dataObserver对象,当外部Adapter的notify…方法被调用时,调用包裹的Adapter的notify…方法。
  3. 这里使用了RecyclerView封装时定义的isBottomRefreshable()方法,来判断是否需要添加底部刷新View,也就是说,如果外部没有调用setOnBottomRefreshListener()设置底部刷新监听,则isBottomRefreshable()将返回false,表示不需要执行底部刷新操作。因此也就不需要添加底部加载View。

5. 支持其他LayoutManager

如前所述,GridLayoutManager和StaggeredGridLayoutManager在实现底部刷新时会和LinearLayoutManager存在一些差异。这里介绍如何支持GridLayoutManager和StaggeredGridLayoutManager的底部刷新功能。

5.1 支持GridLayoutManager

GridLayoutManager是网格布局,允许在一行中有多列,每一列都是一个item。底部刷新View也是一个作为一个item添加到Adapter中,按照之前LinearLayoutManager的实现,它会被安排在最后一行的某一列上,不能占据整行。如图所示。

为了让底部刷新View能够占据整行,需要对GridLayoutManager对象进行设置。GridLayoutManager提供了setSpanSizeLookup()方法,在这里可以设置某个位置的item跨越多列。我们只需要将底部刷新View跨越列数设置为GridLayoutManager的列数即可。

为此,需要重写Adapter的onAttachedToRecyclerView()方法。增加如下代码。

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {super.onAttachedToRecyclerView(recyclerView);// 对Grid布局进行支持RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();if (manager instanceof GridLayoutManager) {final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;final GridLayoutManager.SpanSizeLookup lookup = gridLayoutManager.getSpanSizeLookup();gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {@Overridepublic int getSpanSize(int position) {return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;}});}
}

5.2 支持StaggeredGridLayoutManager

StaggeredGridLayoutManager是瀑布流布局,它同样允许在一行中有多列,因此和GridLayoutManager一样,需要将底部刷新View设置为跨越多列。不过StaggeredGridLayoutManager并没有setSpanSizeLookup方法,如果要设置某个item跨越整行,需要调用item的LayoutParams的setFullSpan()方法。

为此,需要重写Adapter的onViewAttachedToWindow()方法,增加如下代码。

@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {super.onViewAttachedToWindow(holder);if (holder instanceof BottomRefreshViewHolder) {// 支持瀑布流布局ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);}}
}

对StaggeredGridLayoutManager,完成上述代码后,还需要做另外一处修改。

在先前介绍判断底部刷新View是否可见时,是通过LinearLayoutManager的findLastVisibleItemPosition()方法获取当前最后一个可见的item位置来判断的。对StaggeredGridLayoutManager,并没有findLastVisibleItemPosition()方法,但StaggeredGridLayoutManager有一个findLastVisibleItemPositions()方法,它可以用来获取每一列的最后一个可见item的位置,通过将所有列最后一个可见item位置取最大值就可以得取到当前最后一个可见的item位置。

代码如下。

private int getLastVisibleItemPosition() {RecyclerView.LayoutManager manager = getLayoutManager();if (manager instanceof LinearLayoutManager) {return ((LinearLayoutManager) manager).findLastVisibleItemPosition();} else if (manager instanceof StaggeredGridLayoutManager) {int positions[] = ((StaggeredGridLayoutManager) manager).findLastVisibleItemPositions(null);int max = NO_POSITION;for (int pos : positions) {if (pos > max) {max = pos;}}return max;}return NO_POSITION;
}

注意到,这里面只有LinearLayoutManager和StaggeredGridLayoutManager两个分支,并没有GridLayoutManager。这是因为GridLayoutManager是继承自LinearLayoutManager的,所以它包含在了LinearLayoutManager这个分支中,对GridLayoutManager不需要在这里特别处理了。有意思的是StaggeredGridLayoutManager虽然名字中包含了GridLayoutManager,但却并非继承自GridLayoutManager,也没有继承自LinearLayoutManager,所以对StaggeredGridLayoutManager是需要单独处理的。

5.3 支持自定义的LayoutManager

对自定义的LayoutManager,如果是继承自LinearLayoutManager,GridLayoutManager或StaggeredGridLayoutManager,则只需要按照上述说明,实现对GridLayoutManager和StaggeredGridLayoutManager的支持就可以了。如果自定义的LayoutManager是直接继承自RecyclerView.LayoutManager,则需要在自定义的LayoutManager中实现获取当前可见的最后一个item位置的方法,此外,如果自定义的LayoutManager支持一行放置多个item,还需要实现能够将某个item设置为跨越整行的方法。然后参照上述对GridLayoutManager和StaggeredGridLayoutManager的处理,在包裹的Adapter中添加相应的分支即可。

6. 一次滑动过程中避免重复刷新

在设置滑动事件监听时,我们重写了RecyclerView.OnScrollListener对象的onScrolled()方法,在这里判断底部刷新View是否可见,如果底部刷新View可见,则执行底部刷新操作。

这里存在一个问题,设想手指正在屏幕上向上滑动,此时底部刷新View变得可见,于是触发底部刷新操作。但是手指并未立刻离开屏幕,而是继续向下滑动,这时onScrolled()方法仍然会被连续调用,由于底部刷新View仍然可见,所以底部刷新操作也会被不断的触发。再设想另外一种情况,用户触发底部刷新操作后手指立刻离开屏幕,但由于获取数据通常都是异步的过程,从触发底部刷新到获取到数据是需要一定时间的,如果在这段时间内,用户又重新滑动了列表,并使得底部刷新View可见,这时底部刷新操作又会被再一次调用。需要注意的是,这两种情况虽然都是重复触发了底部刷新操作,但是存在一定差异。第一种情况,触发底部刷新操作后手指未离开屏幕,假设这时手指能够完全静止的停留在屏幕上,不会触发新的底部刷新操作。等待一会之后外部完成了数据获取,并更新了RecyclerView,这时手指继续滑动,使得底部刷新View又变得可见,这时仍然会触发底部刷新操作,所以第二种情况并不能够涵盖第一种情况。

一般来说,我们并不希望在一次滑动过程中,触发多次底部刷新操作,也不希望在获取到数据前,再次执行底部刷新操作。为了避免重复刷新,需要在onScrolled()中增加一些额外的处理,同时还需要利用到RecyclerView.OnScrollListener的另外一个方法onScrollStateChanged()。

示例代码如下。注意这里只是列出了相关修改的地方,之前已经提到的部分就省略了,并非完整的代码。

public class XRecyclerView extends RecyclerView {private boolean mBottomRefreshing;private void init() {mBottomRefreshing = false;mOnScrollListener = new RecyclerView.OnScrollListener() {private boolean mAlreadyRefreshed = false;@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);if (newState == RecyclerView.SCROLL_STATE_IDLE) {if (mAlreadyRefreshed) {mAlreadyRefreshed = false;}}}@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);if (isBottomRefreshing() || mAlreadyRefreshed) {return;}if (isBottomViewVisible()) {if (mBottomRefreshListener != null) {mBottomRefreshListener.onBottomRefresh();mBottomRefreshing = true;mAlreadyRefreshed = true;}}}};}public boolean isBottomRefreshing() {return mBottomRefreshing;}public void onBottomRefreshComplete() {mBottomRefreshing = false;}
}

在这段代码中主要是增加了两个变量,mBottomRefreshing和mAlreadyRefreshed。

mBottomRefreshing变量增加在继承的RecyclerView中,表示是否正在刷新。它初始值为false,在onScroll中,如果触发了一次刷新操作,则将其置为true。之后只有在外部调用了onBottomRefreshComplete()才会再次置为false。在onScroll中,触发刷新操作之前增加了判断,如果它为true,则不会再执行刷新操作。这就杜绝了上述第二种情况下的重复刷新,在获取数据完成之前,是不能够再次触发刷新操作的。但是同时要求外部在获取数据完成后,调用这里的onBottomRefreshComplete(),通知RecyclerView数据已经获取好了。

mAlreadyRefreshed变量增加在实现RecyclerView.OnScrollListener接口的匿名内部类中,它表示本次滑动过程中是否已经执行过一次底部刷新操作了。如果触发了一次刷新操作,则将其置为true。在onScrollStateChanged()中,如果滑动状态变为停止滑动,且mAlreadyRefreshed为true,则将其重置为false。在onScroll中,触发刷新操作之前判断mAlreadyRefreshed是否为true,如果为true,则不执行刷新操作。这样就可以避免在一次滑动过程中重复执行底部刷新操作。

7. 底部刷新触发过于敏感问题

在onScroll中,只要底部刷新View一旦可见就会触发底部刷新操作,哪怕只是可见一个像素也会立刻触发。个人觉得这种触发机制过于敏感,用户体验不是很好,尤其是现在用户很多都使用wifi环境来上网,网速都很快,一旦触发底部刷新,很快就能获取到新的数据,然后RecyclerView被更新,导致底部刷新View很难被用户观察到。如果在onScroll中执行底部刷新操作之前增加判断,只有底部刷新View可见到一定程度后,例如有一半可见时才会触发底部刷新操作,这样就可以避免这种问题。不过这个问题并不影响整个功能,如果觉得对用户体验影响不大,完全可以忽略。

示例代码如下。同样的,这里只是列出了相关修改的地方。

private void init() {mOnScrollListener = new RecyclerView.OnScrollListener() {private int mBottomViewVisibleDy = 0;private int mBottomViewHeight = 0;@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);if (newState == RecyclerView.SCROLL_STATE_IDLE) {boolean shouldHide = false;             // 是否需要隐藏bottom view// 如果之前还没有滑到指定的位置就停止了滑动,则将shouldHide置为trueif (mBottomViewVisibleDy != 0) {if (mBottomViewVisibleDy > 0) {shouldHide = true;}mBottomViewVisibleDy = 0;}// 隐藏bottom viewif (shouldHide) {hideBottomView();}}}@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);if (isBottomViewVisible()) {// dy是本次调用onScrolled和上次调用onScrolled在y轴方向的偏移量,这里将bottom view可见之后的偏移量累加起来mBottomViewVisibleDy += dy;if (mBottomViewHeight == 0) {View itemView = getLastVisibleItem();if (itemView != null) {mBottomViewHeight = itemView.getHeight();}}// 如果bottom view可见之后的y轴偏移量大于bottom view高度的一半,则执行bottom refreshif (mBottomViewHeight != 0 && mBottomViewVisibleDy > mBottomViewHeight / 2) {if (mBottomRefreshListener != null) {mBottomRefreshListener.onBottomRefresh();mBottomRefreshing = true;mAlreadyRefreshed = true;}mBottomViewVisibleDy = 0;}} else {mBottomViewVisibleDy = 0;}}};
}private View getLastVisibleItem() {int firstItemPosition = getFirstVisibleItemPosition();int lastItemPosition = getLastVisibleItemPosition();if (firstItemPosition != NO_POSITION && lastItemPosition != NO_POSITION) {return getLayoutManager().getChildAt(lastItemPosition - firstItemPosition);} else {return null;}
}private int getFirstVisibleItemPosition() {RecyclerView.LayoutManager manager = getLayoutManager();if (manager instanceof LinearLayoutManager) {return ((LinearLayoutManager) manager).findFirstVisibleItemPosition();} else if (manager instanceof StaggeredGridLayoutManager) {int positions[] = ((StaggeredGridLayoutManager) manager).findFirstVisibleItemPositions(null);int min = Integer.MAX_VALUE;for (int pos : positions) {if (pos < min) {min = pos;}}return min;}return NO_POSITION;
}// 如果bottom view是可见的,则根据bottom view 当前的位置和RecyclerView当前位置来决定要向上滑动的距离
private void hideBottomView() {if (isBottomViewVisible()) {View bottomView = getLastVisibleItem();if (bottomView != null) {int[] bottomViewLocation = new int[2];bottomView.getLocationInWindow(bottomViewLocation);int[] recyclerViewLocation = new int[2];getLocationInWindow(recyclerViewLocation);int recyclerViewHeight = getHeight();int offset = recyclerViewLocation[1] + recyclerViewHeight - bottomViewLocation[1];if (offset > 0) {scrollBy(0, -offset);}}}
}

在这段代码中,增加了mBottomViewVisibleDy变量,表示底部刷新View可见的高度大小,此外,增加了getLastVisibleItem()方法,用来获取最后一个可见的item,它主要是用来得到底部刷新View的高度。还增加了hideBottomView()方法,它用来隐藏底部刷新View,因为在onScroll中增加了这层判断后,只有底部刷新View可见到一定程度(这里是底部刷新View高度的一半)时才会执行底部刷新操作,如果没有滑到一半就停下来了,那么就需要手动将这显示出来一小半的底部刷新View隐藏起来。

8. 获取到数据后的处理

如前所述,在获取到数据后,需要外部调用onBottomRefreshComplete(),通知RecyclerView数据已经获取好了。在onBottomRefreshComplete()会将mBottomRefreshing置为false。但是仅仅这样处理是不够的。这里还需要做一个额外的操作,就是将底部刷新View隐藏起来。

当外部调用onBottomRefreshComplete()时,即表明本次刷新操作已经完成,这时就不应当再让用户看到这个底部刷新View。然而可能存在一些原因导致这时用户仍然能够看到底部刷新View,这可能是因为获取到的数据量太少,RecyclerView填充新的数据后,也无法将底部刷新View挤到看不见的地方,也可能是因为外部在获取数据后增加了去重的操作,新获取的数据全部都在已经获取的数据里面,导致数据项没有发生变化。无论是哪种情况都不应当再让用户看到底部刷新View,因此需要在onBottomRefreshComplete()中将底部刷新View隐藏起来。

示例代码如下。

private boolean mShouldHideAfterScrollIdle;
private void init() {mOnScrollListener = new RecyclerView.OnScrollListener() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);if (newState == RecyclerView.SCROLL_STATE_IDLE) {boolean shouldHide = false;             // 是否需要隐藏bottom view// 如果需要隐藏bottom view,则将shouldHide置为trueif (mShouldHideAfterScrollIdle) {shouldHide = true;mShouldHideAfterScrollIdle = false;}// 隐藏bottom viewif (shouldHide) {hideBottomView();}}}};
}
public void onBottomRefreshComplete() {mBottomRefreshing = false;// 如果当前没有在滑动状态,则直接隐藏// 如果当前在滑动状态,则等待滑动停止后再隐藏if (getScrollState() == SCROLL_STATE_IDLE) {hideBottomView();mShouldHideAfterScrollIdle = false;} else {mShouldHideAfterScrollIdle = true;}
}

这里借用了之前的hideBottomView()方法来隐藏底部刷新View,同时还需要和滑动状态和onScrollStateChanged()方法相结合。

9. 获取数据失败后的处理

由于获取数据通常都是联网获取,联网获取总是会有获取失败的可能。如果数据获取失败,那么需要做相应的处理。

一般来说有两种处理方式。
1. 外部得知数据获取失败后,显示出错信息。调用onBottomRefreshComplete(),通知RecyclerView刷新完成,RecyclerView内部仍然是之前的处理流程,将mBottomRefreshing置为false,然后隐藏底部刷新View。
2. RecyclerView增加一个方法onBottomRefreshFailed()。外部得知数据获取失败后,调用onBottomRefreshFailed(),在onBottomRefreshFailed()方法中,将mBottomRefreshing置为false,同时在底部刷新View中显示加载出错的信息。

采用方法2需要对底部刷新View进行改造,将其设置为一个TextView和ProgressBar的组合。平时显示ProgressBar,隐藏TextView,在获取数据失败后,改变其状态,隐藏ProgressBar,显示TextView。将TextView文本设置为“加载数据失败”之类的信息。同时,原先的滑动到底部刷新View执行刷新的机制也需要修改,改为点击后刷新。

对方法2虽然需要的改动较多,但总体思路是很明确的。这里就不给出实现代码了。

10. 全部数据获取完成的处理

获取数据的结果在获取成功时还存在另一种特殊情况,就是已经获取到了全部的数据,后面已经没有更多数据了。

为了处理这种情况,可以在RecyclerView中增加setBottomRefreshable()方法,当数据全部获取完成后,调用此方法,通知RecyclerView已经不能够再刷新了。在setBottomRefreshable()中有两种处理方式。

  1. 去掉底部刷新View。这只需要将mBottomRefreshable置为false即可,由于在Adapter有判断此变量的状态(isBottomRefreshable()),如果mBottomRefreshable为false,则不会添加底部刷新View。
  2. 底部刷新View中显示没有更多的信息。

对方法2,同样需要对底部刷新View进行改造,改造方式和之前介绍获取数据失败后的处理方式2一样,将其设置为一个TextView和ProgressBar的组合。当RecyclerView得知数据已经全部获取完毕后,隐藏ProgressBar,显示TextView。将TextView文本设置为“没有更多了”之类的信息。同时,去掉原先的滑动到底部刷新View执行刷新的机制。很多时候数据是在不断变化的,数据已经全部获取完毕只是表示当前的状态,可能过一会之后就会有新的数据产生,这时可以将底部刷新View置为点击后刷新。如果确实不会产生新的数据了,也可以不设置点击刷新操作。

11. 当前数据不足一屏的处理

考虑这样一种情况,当填充完数据后,当前RecyclerView显示不足一屏,由于底部刷新View是作为最后一个item添加的,所以这时底部刷新View将会直接可见。同时由于RecyclerView不足一屏,所以它不能滑动,onScroll也就不会被执行,数据更新操作也就无法触发。

要解决这个问题,通常也有两种方案。
1. 外部第一次获取数据时,尽量多获取一些数据。确保RecyclerView可以填满一屏。虽然这个方案看起来是什么也没做,将问题解决责任推给外部实现。但实际上大多数情况都可以采用这种方式。一般来说,即使要显示的item是非常简单的,例如只有一个icon和一行文本,也只需要十多个就可以填满一屏。外部获取数据时完全可以将一次获取数量设置为20或者更多,这样就可以保证RecyclerView填满一屏。而一次获取20个数据大多数情况完全不会造成负担。所以将数据不足一屏的问题推给外部,是完全合理的。
2. 同样是改造底部刷新View,将其设置为一个TextView和ProgressBar的组合。根据RecyclerView是否可以滑动,决定是显示TextView还是ProgressBar。如果RecyclerView超过一屏,则显示ProgressBar,设置滑动触发刷新,如果RecyclerView不足一屏,则显示TextView,设置点击触发刷新。

要判断RecyclerView是否超过一屏,可以使用RecyclerView提供的computeVerticalScrollOffset()方法,它表示RecyclerView垂直方向偏移量。在加载底部刷新View时,判断其值是否为0,如果为0,表示RecyclerView不足一屏。如果返回值大于0,则表示超过一屏。

12. 支持水平方向的RecyclerView

对水平方向滑动的RecyclerView,上述底部刷新实现的原理和细节处理都是完全一样的,只需要将底部刷新View的布局调整为竖直的,然后将一些和方向有关的接口更改一下即可。例如mBottomViewVisibleDy需要改为mBottomViewVisibleDx,getHeight()改成getWidth()等。这里就不多介绍了。

13. 完整代码

最后将整个实现的完整代码贴在这里。不过这里底部刷新View仍然是ProgressBar,没有改造成ProgressBar和TextView的组合。也就是说,对“获取数据失败后的处理”,“全部数据获取完成的处理”和“当前数据不足一屏的处理”都是采用的方法1。如果需要采用方法2的,可以自行实现。建议将底部刷新View采用自定义View实现,将相关状态封装在自定义View中。此外,这里只支持垂直方向滑动的RecyclerView,对水平方向滑动的RecyclerView,由于没有用到,所以没有实现。

public class XRecyclerView extends RecyclerView {private OnBottomRefreshListener mBottomRefreshListener;private RecyclerView.OnScrollListener mOnScrollListener;private boolean mBottomRefreshing;private boolean mBottomRefreshable;private boolean mShouldHideAfterScrollIdle;public XRecyclerView(Context context) {super(context);init();}public XRecyclerView(Context context, AttributeSet attrs) {super(context, attrs);init();}private void init() {mBottomRefreshing = false;mBottomRefreshable = false;mShouldHideAfterScrollIdle = false;mBottomRefreshListener = null;mOnScrollListener = new RecyclerView.OnScrollListener() {// 此变量为true表示的意思是此轮滑动过程已经执行过一次bottom refresh了// 设想在一次滑动过程中,如果已经执行过一次bottom refresh,这时手指不离开屏幕,// 接着收到bottom refresh结果,将bottom refresh状态置为false,这时如果仍然在滑动,即使bottom view又变得可见了,也不应当再次执行bottom refreshprivate boolean mAlreadyRefreshed = false;private int mBottomViewVisibleDy = 0;private int mBottomViewHeight = 0;@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);if (newState == RecyclerView.SCROLL_STATE_IDLE) {boolean shouldHide = false;             // 是否需要隐藏bottom view// 如果需要隐藏bottom view,则将shouldHide置为trueif (mShouldHideAfterScrollIdle) {shouldHide = true;mShouldHideAfterScrollIdle = false;}// 如果之前还没有滑到指定的位置就停止了滑动,则同样将shouldHide置为trueif (mBottomViewVisibleDy != 0) {if (mBottomViewVisibleDy > 0) {shouldHide = true;}mBottomViewVisibleDy = 0;}// 隐藏bottom viewif (shouldHide) {hideBottomView();}if (mAlreadyRefreshed) {mAlreadyRefreshed = false;}}}@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);// 如果当前不可刷新,或者正在刷新,则不执行bottom refresh操作if (!isBottomRefreshable() || isBottomRefreshing() || mAlreadyRefreshed || dy == 0) {return;}if (isBottomViewVisible()) {// dy是本次调用onScrolled和上次调用onScrolled在y轴方向的偏移量,这里将bottom view可见之后的偏移量累加起来mBottomViewVisibleDy += dy;if (mBottomViewHeight == 0) {View itemView = getLastVisibleItem();if (itemView != null) {mBottomViewHeight = itemView.getHeight();}}// 如果bottom view可见之后的y轴偏移量大于bottom view高度的一半,则执行bottom refreshif (mBottomViewHeight != 0 && mBottomViewVisibleDy > mBottomViewHeight / 2) {if (mBottomRefreshListener != null) {mBottomRefreshListener.onBottomRefresh();mBottomRefreshing = true;mAlreadyRefreshed = true;}mBottomViewVisibleDy = 0;}} else {mBottomViewVisibleDy = 0;}}};}public View getFirstVisibleItem() {return getLayoutManager().getChildAt(0);}public View getSecondVisibleItem() {return getLayoutManager().getChildAt(1);}private View getLastVisibleItem() {int firstItemPosition = getFirstVisibleItemPosition();int lastItemPosition = getLastVisibleItemPosition();if (firstItemPosition != NO_POSITION && lastItemPosition != NO_POSITION) {return getLayoutManager().getChildAt(lastItemPosition - firstItemPosition);} else {return null;}}private int getFirstVisibleItemPosition() {RecyclerView.LayoutManager manager = getLayoutManager();if (manager instanceof LinearLayoutManager) {return ((LinearLayoutManager) manager).findFirstVisibleItemPosition();} else if (manager instanceof StaggeredGridLayoutManager) {int positions[] = ((StaggeredGridLayoutManager) manager).findFirstVisibleItemPositions(null);int min = Integer.MAX_VALUE;for (int pos : positions) {if (pos < min) {min = pos;}}return min;}return NO_POSITION;}private int getLastVisibleItemPosition() {RecyclerView.LayoutManager manager = getLayoutManager();if (manager instanceof LinearLayoutManager) {return ((LinearLayoutManager) manager).findLastVisibleItemPosition();} else if (manager instanceof StaggeredGridLayoutManager) {int positions[] = ((StaggeredGridLayoutManager) manager).findLastVisibleItemPositions(null);int max = NO_POSITION;for (int pos : positions) {if (pos > max) {max = pos;}}return max;}return NO_POSITION;}private boolean isBottomViewVisible() {int lastVisibleItem = getLastVisibleItemPosition();return lastVisibleItem != NO_POSITION && lastVisibleItem == getAdapter().getItemCount() - 1;}// 滑到顶部public void gotoTop() {smoothScrollToPosition(0);}// 设置为没有数据了public void setBottomRefreshable(boolean refreshable) {mBottomRefreshable = refreshable;getAdapter().notifyDataSetChanged();}public boolean isBottomRefreshable() {return mBottomRefreshable;}@Overridepublic void setAdapter(RecyclerView.Adapter adapter) {if (adapter != null) {WrapperAdapter wrapperAdapter = new WrapperAdapter(adapter);super.setAdapter(wrapperAdapter);}}// 设置底部下拉刷新监听public void setOnBottomRefreshListener(OnBottomRefreshListener listener) {mBottomRefreshListener = listener;if (mBottomRefreshListener != null) {addOnScrollListener(mOnScrollListener);mBottomRefreshable = true;} else {removeOnScrollListener(mOnScrollListener);mBottomRefreshable = false;}}// 当前是否正在bottom refreshingpublic boolean isBottomRefreshing() {return mBottomRefreshing;}// 下拉刷新完成之后需要隐藏bottom viewpublic void onBottomRefreshComplete() {mBottomRefreshing = false;// 如果当前没有在滑动状态,则直接隐藏// 如果当前在滑动状态,则等待滑动停止后再隐藏if (getScrollState() == SCROLL_STATE_IDLE) {hideBottomView();mShouldHideAfterScrollIdle = false;} else {mShouldHideAfterScrollIdle = true;}}// 隐藏bottom view// 如果bottom view是可见的,则根据bottom view 当前的位置和RecyclerView当前位置来决定要向上滑动的距离private void hideBottomView() {if (isBottomViewVisible()) {View bottomView = getLastVisibleItem();if (bottomView != null) {int[] bottomViewLocation = new int[2];bottomView.getLocationInWindow(bottomViewLocation);int[] recyclerViewLocation = new int[2];getLocationInWindow(recyclerViewLocation);int recyclerViewHeight = getHeight();int offset = recyclerViewLocation[1] + recyclerViewHeight - bottomViewLocation[1];if (offset > 0) {scrollBy(0, -offset);}}}}public interface OnBottomRefreshListener {void onBottomRefresh();}/*** 自定义包裹的Adapter,主要用来处理加载更多视图*/private class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;/*** 被包裹的外部Adapter*/private RecyclerView.Adapter mInnerAdapter;private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {@Overridepublic void onChanged() {super.onChanged();notifyDataSetChanged();}@Overridepublic void onItemRangeChanged(int positionStart, int itemCount) {super.onItemRangeChanged(positionStart, itemCount);notifyItemRangeChanged(positionStart, itemCount);}@Overridepublic void onItemRangeInserted(int positionStart, int itemCount) {super.onItemRangeInserted(positionStart, itemCount);notifyItemRangeInserted(positionStart, itemCount);}@Overridepublic void onItemRangeRemoved(int positionStart, int itemCount) {super.onItemRangeRemoved(positionStart, itemCount);notifyItemRangeRemoved(positionStart, itemCount);}@Overridepublic void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {super.onItemRangeMoved(fromPosition, toPosition, itemCount);notifyItemRangeChanged(fromPosition, toPosition + itemCount);}};private WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {if (mInnerAdapter != null) {notifyItemRangeRemoved(0, mInnerAdapter.getItemCount());mInnerAdapter.unregisterAdapterDataObserver(dataObserver);}this.mInnerAdapter = adapter;mInnerAdapter.registerAdapterDataObserver(dataObserver);notifyItemRangeInserted(0, mInnerAdapter.getItemCount());}public boolean isLoadMoreView(int position) {return isBottomRefreshable() && position == getItemCount() - 1;}@Overridepublic int getItemCount() {if (mInnerAdapter != null) {int itemCount = mInnerAdapter.getItemCount();if (isBottomRefreshable()) {return itemCount + 1;} else {return itemCount;}} else {return 0;}}@Overridepublic int getItemViewType(int position) {if (isBottomRefreshable()) {if (mInnerAdapter != null) {int adapterCount = mInnerAdapter.getItemCount();if (position < adapterCount) {return mInnerAdapter.getItemViewType(position);}}return TYPE_BOTTOM_REFRESH_ITEM;} else {if (mInnerAdapter != null) {return mInnerAdapter.getItemViewType(position);} else {return super.getItemViewType(position);}}}@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {if (viewType == TYPE_BOTTOM_REFRESH_ITEM) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_bottom_refresh_item, parent, false);return new BottomRefreshViewHolder(view);} else {return mInnerAdapter.onCreateViewHolder(parent, viewType);}}@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {if (!(holder instanceof BottomRefreshViewHolder)) {if (mInnerAdapter != null) {int adapterCount = mInnerAdapter.getItemCount();if (position <adapterCount) {mInnerAdapter.onBindViewHolder(holder, position);}}}}@Overridepublic void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {super.onViewAttachedToWindow(holder);if (holder instanceof BottomRefreshViewHolder) {// 支持瀑布流布局ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);}}}@Overridepublic void onAttachedToRecyclerView(RecyclerView recyclerView) {super.onAttachedToRecyclerView(recyclerView);// 对Grid布局进行支持RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();if (manager instanceof GridLayoutManager) {final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;final GridLayoutManager.SpanSizeLookup lookup = gridLayoutManager.getSpanSizeLookup();gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {@Overridepublic int getSpanSize(int position) {return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;}});}}}/*** bottom refresh View对应的ViewHolder*/private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {BottomRefreshViewHolder(View itemView) {super(itemView);}}
}

RecyclerView底部刷新实现详解相关推荐

  1. recyclervie刷新到底部_RecyclerView底部刷新实现详解

    关于RecyclerView底部刷新实现的文章其实已经很多了,但大都只介绍了其基本原理和框架,对其中的很多细节没有交代,无法直接使用.本文会着重介绍RecyclerView底部刷新实现的一些细节处理. ...

  2. ajax 页面无刷新,Ajax的页面无刷新实现详解(附代码)

    这次给大家带来Ajax的页面无刷新实现详解(附代码),Ajax页面无刷新实现的注意事项有哪些,下面就是实战案例,一起来看一下. ajax (ajax开发) AJAX即"Asynchronou ...

  3. 紫乌鸦服务器维护后多久刷,魔兽世界黑暗之星的灵爪飞鹰获取流程攻略 紫乌鸦坐骑刷新机制详解[多图]...

    魔兽世界中的黑暗之星的灵爪飞鹰坐骑很多玩家不太了解获取的详细方法,下面带给大家的是魔兽世界黑暗之星的灵爪飞鹰紫乌鸦坐骑刷新机制详解. 紫乌鸦坐骑刷新机制 1. 如何获得 在德拉诺任意区域(除了塔纳安丛 ...

  4. [Android] DiffUtil在RecyclerView中的使用详解

    概述 DiffUtil是recyclerview support library v7 24.2.0版本中新增的类,根据Google官方文档的介绍,DiffUtil的作用是比较两个数据列表并能计算出一 ...

  5. linux下的arp刷新命令详解,linux arp 命令常用参数详解

    linux arp 命令常用参数详解 linux arp 命令常用参数详解 显示和修改地址解析协议(ARP)使用的"IP 到物理"地址转换表. ARP -s inet_addr e ...

  6. Android控件知识点-RecyclerView(全网最详解)

    背景:笔者在开发Android 蓝牙通信,需要搜索蓝牙设备并且动态显示,这个时候就需要选取适合的相关视图控件了,最后兜兜转转还是觉得RecyclerView好用一些,本人也是在开发的过程中,搜索了很多 ...

  7. Android RecyclerView最全使用详解

    本文目录 RecyclerView概述 RecyclerView使用-基础篇 第一步:添加RecyclerView 第二步:添加布局文件 第三步:添加逻辑代码 运行效果 RecyclerView使用- ...

  8. ViewPager刷新问题详解

    原文链接:简书diygreen,http://www.jianshu.com/p/266861496508 一.PagerAdapter介绍 先看效果图 PagerAdapter简介 ListView ...

  9. Windows窗口刷新机制详解

    1.Windows的窗口刷新管理 窗口句柄(HWND)都是由操作系统内核管理的,系统内部有一个z-order序列,记录着当前窗口从屏幕底部(假象的从屏幕到眼睛的方向),到屏幕最高层的一个窗口句柄的排序 ...

最新文章

  1. python自动测试p-Python自动化测试
  2. windows平台下安装Mysql8.0.20版本
  3. aspx 判断字符串是否为decimal_python 经典面试题:判断字符串括号是否闭合{}[]()...
  4. python numpy中sum()时出现负值
  5. 零基础学编程,如何区分C语言和Java?我们到底如何怎么进行选择!
  6. 新版 chrome 将原生支持图片懒加载!
  7. Spring学习总结(1)- IOC
  8. Resource Monitor的使用和理解
  9. GROUP BY中ROLLUP/CUBE/GROUPING/GROUPING SETS使用示例
  10. java引入math包_JAVA math包
  11. 申请苹果公司版开发者账号实录【99美元,非299美元企业版账号】
  12. 尚德机构季报图解:精细化运营降本增效 净利达1.15亿
  13. 2011年第36届大连赛区现场赛Board
  14. Linux 内核同步(一):原子操作
  15. CUDA out of memory解决办法
  16. 博士延期的我,女友已经是副教授了
  17. linux服务器安装zip解压缩工具
  18. JSONViewer下载路径
  19. 使用iperf进行局域网内测速
  20. mybatis中0被当成空值

热门文章

  1. 芳香之城传奇的美丽神话故事
  2. 上古卷轴java怎么刷_上古卷轴5快速升级方法一览 教你如何快速升级
  3. 大小写字母转换、数字与字符转换及ASCII码表
  4. 手游聊天软件遇到大型攻击怎么办?游戏盾SDK清洗无忧
  5. iis服务器配置微信php,IIS+PHP下微信二次开发JS-SDK签名失败(已解决)
  6. u8不显示登录到服务器,用友U8 不能登录到服务器
  7. 最值得收藏的电脑快捷键使用, 让你的效果成倍增加
  8. python采用的是基于_Python采用的是基于____的内存管理方式
  9. CentOS 8 更新提示 appstream 错误
  10. SolidWorks软件小技巧之命令操作规则