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

顶部刷新和底部刷新

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

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

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

RecyclerView底部刷新的原理

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

RecyclerView底部刷新的一般实现

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

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

设置RecyclerView的滑动事件监听,在滑动过程中,根据底部View是否可见,决定是否执行底部刷新操作

执行底部刷新时,获取更多数据

获取完数据后,通知Adapter更新RecyclerView

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

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

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

表示加载中的View

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

android:layout_width="match_parent"

android:layout_height="40dp">

android:layout_gravity="center"

android:layout_width="30dp"

android:layout_height="30dp"/>

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

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

为RecyclerView添加底部刷新View

为RecyclerView添加底部刷新View一般是通过将底部刷新View作为RecyclerView的Item来实现的。

为此需要改写RecyclerView Adapter的以下几个方法。

getItemCount

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

例如:

@Override

public int getItemCount() {

return mList.size() + 1;

}

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;

}

}

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对应的ViewHolder

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);

}

// 如果是其他类型的View,则按照正常流程创建普通的ViewHolder

else {

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_normal_item, parent, false);

return new NormalViewHolder(view);

}

}

onBindViewHolder

RecyclerView Adapter的onBindViewHolder方法用来将ViewHolder和对应的数据绑定起来。由于底部刷新View并不需要绑定任何数据,所以这里不需要对底部刷新ViewHolder做特别的处理,只需要判断下是否是底部刷新ViewHolder就可以了。

例如:

@Override

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

if (!(holder instanceof BottomRefreshViewHolder)) {

...

}

}

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

滑动事件处理

设置滑动事件监听

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() {

@Override

public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

super.onScrolled(recyclerView, dx, dy);

if (isBottomViewVisible()) {

requestMoreData();

}

}

};

addOnScrollListener(onScrollListener);

获取更多数据

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

更新RecyclerView

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

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

底部刷新功能的封装

上述底部刷新功能的实现,包含了两部分的修改,一部分是对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来实现。

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

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() {

@Override

public 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中用到。

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 {

private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;

/**

* 被包裹的外部Adapter

*/

private RecyclerView.Adapter mInnerAdapter;

private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {

@Override

public void onChanged() {

super.onChanged();

notifyDataSetChanged();

}

@Override

public void onItemRangeChanged(int positionStart, int itemCount) {

super.onItemRangeChanged(positionStart, itemCount);

notifyItemRangeChanged(positionStart, itemCount);

}

@Override

public void onItemRangeInserted(int positionStart, int itemCount) {

super.onItemRangeInserted(positionStart, itemCount);

notifyItemRangeInserted(positionStart, itemCount);

}

@Override

public void onItemRangeRemoved(int positionStart, int itemCount) {

super.onItemRangeRemoved(positionStart, itemCount);

notifyItemRangeRemoved(positionStart, itemCount);

}

@Override

public 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;

}

@Override

public int getItemCount() {

if (mInnerAdapter != null) {

int itemCount = mInnerAdapter.getItemCount();

if (isBottomRefreshable()) {

return itemCount + 1;

} else {

return itemCount;

}

} else {

return 0;

}

}

@Override

public 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);

}

}

}

@Override

public 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);

}

}

@Override

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

if (!(holder instanceof BottomRefreshViewHolder)) {

if (mInnerAdapter != null) {

int adapterCount = mInnerAdapter.getItemCount();

if (position

mInnerAdapter.onBindViewHolder(holder, position);

}

}

}

}

}

/**

* bottom refresh View对应的ViewHolder

*/

private class BottomRefreshViewHolder extends RecyclerView.ViewHolder {

BottomRefreshViewHolder(View itemView) {

super(itemView);

}

}

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

底部刷新View类型对应的整数值不能和外部Adapter已有的类型值重复,由于无法事先知道外部Adapter会定义哪些类型,所以这里为底部刷新View定义了一个相对来说比较特殊的数值Integer.MIN_VALUE + 1,但如果在实际使用时,发现外部Adapter也定义了这种类型,则需要修改这个数值,或者修改外部Adapter的定义。

由于外部数据更新时都是调用外部Adapter的notify…方法来通知RecyclerView更新,所以这里定义了dataObserver对象,当外部Adapter的notify…方法被调用时,调用包裹的Adapter的notify…方法。

这里使用了RecyclerView封装时定义的isBottomRefreshable()方法,来判断是否需要添加底部刷新View,也就是说,如果外部没有调用setOnBottomRefreshListener()设置底部刷新监听,则isBottomRefreshable()将返回false,表示不需要执行底部刷新操作。因此也就不需要添加底部加载View。

支持其他LayoutManager

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

支持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() {

@Override

public int getSpanSize(int position) {

return isLoadMoreView(position) ? gridLayoutManager.getSpanCount() : lookup.getSpanSize(position) ;

}

});

}

}

支持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是需要单独处理的。

支持自定义的LayoutManager

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

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

在设置滑动事件监听时,我们重写了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;

@Override

public void onScrollStateChanged(RecyclerView recyclerView, int newState) {

super.onScrollStateChanged(recyclerView, newState);

if (newState == RecyclerView.SCROLL_STATE_IDLE) {

if (mAlreadyRefreshed) {

mAlreadyRefreshed = false;

}

}

}

@Override

public 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,则不执行刷新操作。这样就可以避免在一次滑动过程中重复执行底部刷新操作。

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

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

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

private void init() {

mOnScrollListener = new RecyclerView.OnScrollListener() {

private int mBottomViewVisibleDy = 0;

private int mBottomViewHeight = 0;

@Override

public void onScrollStateChanged(RecyclerView recyclerView, int newState) {

super.onScrollStateChanged(recyclerView, newState);

if (newState == RecyclerView.SCROLL_STATE_IDLE) {

boolean shouldHide = false; // 是否需要隐藏bottom view

// 如果之前还没有滑到指定的位置就停止了滑动,则将shouldHide置为true

if (mBottomViewVisibleDy != 0) {

if (mBottomViewVisibleDy > 0) {

shouldHide = true;

}

mBottomViewVisibleDy = 0;

}

// 隐藏bottom view

if (shouldHide) {

hideBottomView();

}

}

}

@Override

public 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 refresh

if (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隐藏起来。

获取到数据后的处理

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

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

示例代码如下。

private boolean mShouldHideAfterScrollIdle;

private void init() {

mOnScrollListener = new RecyclerView.OnScrollListener() {

@Override

public void onScrollStateChanged(RecyclerView recyclerView, int newState) {

super.onScrollStateChanged(recyclerView, newState);

if (newState == RecyclerView.SCROLL_STATE_IDLE) {

boolean shouldHide = false; // 是否需要隐藏bottom view

// 如果需要隐藏bottom view,则将shouldHide置为true

if (mShouldHideAfterScrollIdle) {

shouldHide = true;

mShouldHideAfterScrollIdle = false;

}

// 隐藏bottom view

if (shouldHide) {

hideBottomView();

}

}

}

};

}

public void onBottomRefreshComplete() {

mBottomRefreshing = false;

// 如果当前没有在滑动状态,则直接隐藏

// 如果当前在滑动状态,则等待滑动停止后再隐藏

if (getScrollState() == SCROLL_STATE_IDLE) {

hideBottomView();

mShouldHideAfterScrollIdle = false;

} else {

mShouldHideAfterScrollIdle = true;

}

}

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

获取数据失败后的处理

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

一般来说有两种处理方式。

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虽然需要的改动较多,但总体思路是很明确的。这里就不给出实现代码了。

全部数据获取完成的处理

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

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

隐藏底部刷新View。这只需要将mBottomRefreshable置为false即可,由于在Adapter有判断此变量的状态(isBottomRefreshable()),如果mBottomRefreshable为false,则不会添加底部刷新View。

底部刷新View中显示没有更多的信息。

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

当前数据不足一屏的处理

考虑这样一种情况,当填充完数据后,当前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()方法,如果它返回0,则表示RecyclerView不能滑动,也就是不足一屏。如果返回值大于0,则表示超过一屏。

支持水平方向的RecyclerView

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

完整代码

最后将整个实现的完整代码贴在这里。不过这里底部刷新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 refresh

private boolean mAlreadyRefreshed = false;

private int mBottomViewVisibleDy = 0;

private int mBottomViewHeight = 0;

@Override

public void onScrollStateChanged(RecyclerView recyclerView, int newState) {

super.onScrollStateChanged(recyclerView, newState);

if (newState == RecyclerView.SCROLL_STATE_IDLE) {

boolean shouldHide = false; // 是否需要隐藏bottom view

// 如果需要隐藏bottom view,则将shouldHide置为true

if (mShouldHideAfterScrollIdle) {

shouldHide = true;

mShouldHideAfterScrollIdle = false;

}

// 如果之前还没有滑到指定的位置就停止了滑动,则同样将shouldHide置为true

if (mBottomViewVisibleDy != 0) {

if (mBottomViewVisibleDy > 0) {

shouldHide = true;

}

mBottomViewVisibleDy = 0;

}

// 隐藏bottom view

if (shouldHide) {

hideBottomView();

}

if (mAlreadyRefreshed) {

mAlreadyRefreshed = false;

}

}

}

@Override

public 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 refresh

if (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;

}

@Override

public 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 refreshing

public boolean isBottomRefreshing() {

return mBottomRefreshing;

}

// 下拉刷新完成之后需要隐藏bottom view

// 一般来说,如果下拉刷新完成后获取到了更多的数据,这时填充的数据会自动将bottom view挤到后面看不见的位置

// 但是,如果下拉刷新没有获取到更多的数据,这可能是已经恰好没有任何数据了,但也可能是接口返回错误(例如服务器连接不上,网络问题等)

// 如果是第一种情况,则会通过setBottomRefreshable(false)取消bottom view

// 但如果是第二种情况,由于没有更多数据的填充,也不能取消bottom view,这时bottom view就会一直显示在这里,所以就需要手动将其隐藏起来

public 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 {

private static final int TYPE_BOTTOM_REFRESH_ITEM = Integer.MIN_VALUE + 1;

/**

* 被包裹的外部Adapter

*/

private RecyclerView.Adapter mInnerAdapter;

private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {

@Override

public void onChanged() {

super.onChanged();

notifyDataSetChanged();

}

@Override

public void onItemRangeChanged(int positionStart, int itemCount) {

super.onItemRangeChanged(positionStart, itemCount);

notifyItemRangeChanged(positionStart, itemCount);

}

@Override

public void onItemRangeInserted(int positionStart, int itemCount) {

super.onItemRangeInserted(positionStart, itemCount);

notifyItemRangeInserted(positionStart, itemCount);

}

@Override

public void onItemRangeRemoved(int positionStart, int itemCount) {

super.onItemRangeRemoved(positionStart, itemCount);

notifyItemRangeRemoved(positionStart, itemCount);

}

@Override

public 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;

}

@Override

public int getItemCount() {

if (mInnerAdapter != null) {

int itemCount = mInnerAdapter.getItemCount();

if (isBottomRefreshable()) {

return itemCount + 1;

} else {

return itemCount;

}

} else {

return 0;

}

}

@Override

public 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);

}

}

}

@Override

public 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);

}

}

@Override

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

if (!(holder instanceof BottomRefreshViewHolder)) {

if (mInnerAdapter != null) {

int adapterCount = mInnerAdapter.getItemCount();

if (position

mInnerAdapter.onBindViewHolder(holder, position);

}

}

}

}

@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);

}

}

}

@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() {

@Override

public 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);

}

}

}

recyclervie刷新到底部_RecyclerView底部刷新实现详解相关推荐

  1. 清理localstorage_vue 界面刷新数据被清除 localStorage的使用详解

    localStorage是html5新增的一个本地存储API,它有5M的大小空间,通过(key,value)的方式存储在浏览器中 window.localStorage.setItem('key', ...

  2. android底部滑动出现虚拟按键,Android适配底部虚拟按键的方法详解

    Android适配底部虚拟按键的方法详解 发布时间:2020-10-09 05:26:12 来源:脚本之家 阅读:171 作者:yuanzhihui123 最近项目进行适配的时候发现部分(如华为手机) ...

  3. qml 不刷新 放大还原_【显示器选择详解】你的电脑能否带动高分辨率,高刷新率显示器?...

    引导:1.尺寸及分辨率选择 2.面板类型选择 3.曲面屏和直面屏幕选择 4.刷新率选择 5.亮度选择 6.色域.色深 7.还有一个问的最多的问题:电脑是否能够带动显示器?-接口类型解释 1. 尺寸及分 ...

  4. dorehtml.php,帝国cms后台实现刷新多栏目内容页的方法详解

    以下代码适用于帝国CMS6.6版,7.0版不同处在代码注释处已标明. 找到e/admin/ChangeData.php文件 原先的栏目是单选菜单 所有栏目 =$class?> 改为: 所有栏目 ...

  5. android verticalviewpager 下拉刷新 上拉加载_设计规范 | 详解组件控件结构体系:加载类...

    设计师在进行APP设计时,往往会更加专注于界面的布局.界面和界面之间怎么跳转.操作反馈,却往往忽略掉一个比较重要的环节,就是APP数据加载中的设计.那么我们怎么处理好界面交互中的加载设计,保证体验无缝 ...

  6. android 刷新标题栏,Android 自定义标题栏的实例详解

    Android 自定义标题栏的实例详解 开发 Android APP 经常会用到自定义标题栏,而有多级页面的情况下还需要给自定义标题栏传递数据. 本文要点: 自定义标题填充不完整 自定义标题栏返回按钮 ...

  7. 探索SwipeRefreshLayout配合自定义ListView完成下拉刷新、滑到底部自动加载更多

    在Android开发过程中经常需要实现上下拉刷新功能,Google推出的下拉刷新控件SwipeRefreshLayout(彩虹条),由于官方版本只有下拉刷新而没有上拉加载更多的功能,很多人也尝试在这个 ...

  8. RecyclerView底部刷新实现详解

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

  9. Android PullToRefresh(下拉刷新)的使用详解

    开源项地址:https://github.com/chrisbanes/Android-PullToRefresh 在Android-PullToRefresh-master文件夹下,我们会看到还有三 ...

最新文章

  1. python计算选手最后得分并取出前三名_在Python中存储最后3个分数并删除旧分数并计算平均值?...
  2. 周末,我打开《逆水寒》参加了一场AI学术会议
  3. linux用户管理最常用的三个文件说明(不完整版)
  4. 大二c语言数电课程设计,数电课程设计报告(数字钟的设计).doc
  5. clone远程代码 在不同电脑上git_用树莓派3搭建私有git代码仓库
  6. phpcms v9 的表单向导功能的使用方法
  7. MaxCompute - ODPS重装上阵 第四弹 - CTE,VALUES,SEMIJOIN
  8. 云计算监控—Prometheus监控系统(文末赠书)
  9. centos 安装git_在Centos8上安装Git的方法
  10. 用js实现一个无限循环的动画
  11. “Master”连胜世界围棋冠军,谁是幕后智能引擎?
  12. 带进度条的Flash多文件上传面板(SwfUploadPanel) (转载)
  13. hardlockup的原理说明
  14. 最简单的推荐系统实践
  15. 笑出腹肌的程序猿搞笑趣图
  16. 条形码编码规则及标准
  17. 贾俊平统计学思维导图- 第十三章 时间序列分析和预测
  18. 一个Unity3D制作的坦克游戏——《燃烧的地平线》
  19. 简单的html图片上传工具
  20. 关于knife4j工具聚合api文档的使用

热门文章

  1. Python 学习笔记(1)Hello Python
  2. 基于FPGA的图像增强系统的verilog开发(3000+字)
  3. 张学友演唱会成犯罪分子噩梦,阿里云云盾人发布脸对比功能将进一步提升罪犯监察力度...
  4. zabbix企业应用之bind dns监控(转)
  5. 关于“习惯”的精彩分析
  6. 静态数组的声明与例子练习
  7. jquery.autocomplete.js 插件的自定义搜索规则
  8. IDEA2021.03 项目全部变红,但是可以正常编译运行
  9. CSP认证201604-4 游戏[C++题解]:bfs、拆点、迷宫问题加强版、三维数组
  10. php程序监控指标,通达信主力动向监控副图指标公式