关于RecyclerView底部刷新实现的文章已经很多了,但大都只介绍了其基本原理和框架,对其中的很多细节没有交代,无法直接使用。本文会着重介绍RecyclerView底部刷新实现的一些细节处理。
1. 顶部刷新和底部刷新
顶部刷新和底部刷新都是列表中两种常见的交互方式。顶部刷新通常对应更新数据,更新的数据会替换掉当前数据。而底部刷新则对应获取更多的数据,更多的数据会添加在当前数据的后面。
顶部刷新和底部刷新在其他文章中更多的被称为下拉刷新和上拉加载更多,不过个人并不喜欢这样的称谓,每次提及上拉和下拉时都会感觉很困惑,需要思考一下上拉和下拉究竟对应哪个操作。所以这里将这两种操作称为顶部刷新和底部刷新。当然如果读者没有这个困扰,觉得很容易区分上拉和下拉,不烦还是延续这种称谓。
本文只会介绍底部刷新,对顶部刷新会在后面的文章中再介绍。
2. RecyclerView底部刷新的原理
RecyclerView底部刷新的原理很简单,就是在RecyclerView最底部添加一个表示加载中的View,然后监听RecyclerView的滑动事件,当RecyclerView滑动时,判断是否滑动到了RecyclerView的底部,也就是最后一个加载中的View是否可见,如果滑动到了RecyclerView底部,则执行底部刷新操作,获取更多数据。最后当获取更多数据完成后,更新RecyclerView的Adapter。
3. RecyclerView底部刷新的一般实现
根据上述RecyclerView底部刷新的实现原理,可以知道RecyclerView底部刷新实际上包含如下步骤。注意这里的步骤并不代表代码的书写顺序,它更多的表示的是代码执行的顺序。
- 为RecyclerView底部添加一个表示加载中的View
- 设置RecyclerView的滑动事件监听,在滑动过程中,根据底部View是否可见,决定是否执行底部刷新操作
- 执行底部刷新时,获取更多数据
- 获取完数据后,通知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的以下几个方法。
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
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() {
@Override
public 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() {
@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() {