上拉加载无需监听滑动事件,可自定义加载布局,显示异常提示,自定义异常提示。
此篇文章为BaseRecyclerViewAdapterHelper源码解读第四篇,开源库地址,如果没有看过之前3篇文章的同学可以先去看看,大神可直接跳过.
BaseRecyclerViewAdapterHelper源码解读(一) 封装简单的adapter和万能的BaseViewHolder
BaseRecyclerViewAdapterHelper源码解读(二) 添加header和footer
BaseRecyclerViewAdapterHelper源码解读(三) 添加动画
今天给大家带来BaseRecyclerViewAdapterHelper是如何添加动画的.由于本人才学尚浅,如有有不对的地方,欢迎指正,谢谢.
// 滑动最后一个Item的时候回调onLoadMoreRequested方法
setOnLoadMoreListener(RequestLoadMoreListener);
默认第一次加载会回调onLoadMoreRequested()加载更多这个方法,如果不需要可以配置如下:
mQuickAdapter.disableLoadMoreIfNotFullPage();
回调处理代码
mQuickAdapter.setOnLoadMoreListener(new BaseQuickAdapter.RequestLoadMoreListener() {
@Override public void onLoadMoreRequested() {
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
if (mCurrentCounter >= TOTAL_COUNTER) {
//数据全部加载完毕
mQuickAdapter.loadMoreEnd();
} else {
if (isErr) {
//成功获取更多数据
mQuickAdapter.addData(DataServer.getSampleData(PAGE_SIZE));
mCurrentCounter = mQuickAdapter.getData().size();
mQuickAdapter.loadMoreComplete();
} else {
//获取更多数据失败
isErr = true;
Toast.makeText(PullToRefreshUseActivity.this, R.string.network_err, Toast.LENGTH_LONG).show();
mQuickAdapter.loadMoreFail();
}
}
}
}, delayMillis);
}
}, mReyclerView);
这里可能看的不是很清楚,详情请看官方demo,https://github.com/CymChad/BaseRecyclerViewAdapterHelper/blob/d296d1fb4e7a64b9fa8a2601f3f896d3a9518be5/app/src/main/java/com/chad/baserecyclerviewadapterhelper/PullToRefreshUseActivity.java
加载完成(注意不是加载结束,而是本次数据加载结束并且还有下页数据)
mQuickAdapter.loadMoreComplete();
加载失败
mQuickAdapter.loadMoreFail();
加载结束
mQuickAdapter.loadMoreEnd();
/**
* 设置监听RecyclerView上拉加载更多 并设置监听器
* @param requestLoadMoreListener
* @param recyclerView
*/
public void setOnLoadMoreListener(RequestLoadMoreListener requestLoadMoreListener,
RecyclerView recyclerView) {
openLoadMore(requestLoadMoreListener);
if (getRecyclerView() == null) {
setRecyclerView(recyclerView);
}
}
/**
*
* 开启上拉加载更多
* @param requestLoadMoreListener
*/
private void openLoadMore(RequestLoadMoreListener requestLoadMoreListener) {
this.mRequestLoadMoreListener = requestLoadMoreListener;
mNextLoadEnable = true;
mLoadMoreEnable = true;
mLoading = false;
}
/**
* 设置当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法
* @param preLoadNumber
*/
public void setPreLoadNumber(int preLoadNumber) {
if (preLoadNumber > 1) {
mPreLoadNumber = preLoadNumber;
}
}
先来说简单的,上面这个方法比较简单,属于配置型的方法.
就是设置当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法.待会儿下面会用到这个参数,先放着.
另外,这个方法可以在使用时不必调用,因为已经有默认值了.
/**
* To bind different types of holder and solve different the bind events
*
* @param holder
* @param position
* @see #getDefItemViewType(int)
*/
@Override
public void onBindViewHolder(K holder, int position) {
//Add up fetch logic, almost like load more, but simpler.
//这里是判断是否需要下拉加载更多
autoUpFetch(position);
//Do not move position, need to change before LoadMoreView binding
//判断是否需要进行上拉加载更多
autoLoadMore(position);
int viewType = holder.getItemViewType();
switch (viewType) {
case 0:
convert(holder, getItem(position - getHeaderLayoutCount()));
break;
case LOADING_VIEW:
mLoadMoreView.convert(holder);
break;
case HEADER_VIEW:
break;
case EMPTY_VIEW:
break;
case FOOTER_VIEW:
break;
default:
convert(holder, getItem(position - getHeaderLayoutCount()));
break;
}
}
/**
* 根据position位置判断当前是否需要进行加载更多
*
* @param position 当前onBindViewHolder()的Position
*/
private void autoLoadMore(int position) {
// 判断是否可以进行加载更多的逻辑
if (getLoadMoreViewCount() == 0) {
return;
}
//只有在当前列表的倒数mPreLoadNumber个item开始绑定数据时才进行加载更多的逻辑
if (position < getItemCount() - mPreLoadNumber) {
return;
}
//判断当前加载状态,如果不是默认状态(可能正处于 正在加载中 的状态),则不进行加载
if (mLoadMoreView.getLoadMoreStatus() != LoadMoreView.STATUS_DEFAULT) {
return;
}
//设置当前状态:加载中
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_LOADING);
if (!mLoading) {
mLoading = true;
if (getRecyclerView() != null) {
getRecyclerView().post(new Runnable() {
@Override
public void run() {
//回调 让调用者去处理加载更多的逻辑
mRequestLoadMoreListener.onLoadMoreRequested();
}
});
} else {
mRequestLoadMoreListener.onLoadMoreRequested();
}
}
}
/**
* Load more view count
* 判断是否可以进行加载更多的逻辑
* @return 0 or 1
*/
public int getLoadMoreViewCount() {
//参数合法性 加载更多状态
if (mRequestLoadMoreListener == null || !mLoadMoreEnable) {
return 0;
}
//可加载下一页 有无更多数据
if (!mNextLoadEnable && mLoadMoreView.isLoadEndMoreGone()) {
return 0;
}
//当前数据项个数
if (mData.size() == 0) {
return 0;
}
return 1;
}
重点来了,加载更多的主要逻辑就在这里:当在onBindViewHolder()的时候,根据当前item的position位置,然后去判断是否应该执行加载更多.
具体判断逻辑:当一个item第一次进入window界面时,会调用onBindViewHolder()去绑定数据,这个时候我们知道该position的位置,
于是我们就可以这样干:设置一个mPreLoadNumber标志位置( 当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法 ),
当onBindViewHolder()在绑定数据时的position是最后mPreLoadNumber个时,我们即进行加载更多的回调,然后让调用者去处理.
当然,在回调之前,我们需要进行一些判断,确定当前是否可以进行加载更多.
- mRequestLoadMoreListener监听器是否为null,当前是否处于可以加载更多的状态(mLoadMoreEnable标志位控制)
- 当前有无更多数据(这个由外界调用者决定)
- 当前的数据项个数是否为0,如果没有数据项,那就不必加载更多
- 是否进入倒数的那mPreLoadNumber区域
- 判断当前(mLoadMoreView 这是加载更多的View )加载状态,如果不是默认状态(可能正处于 正在加载中 的状态),则不进行加载
好吧,细心的观众可能已经发现了,上面的这种方式其实有一个缺点:当数据项个数小于1屏幕,那么最后倒数的mPreLoadNumber个肯定是可见的,既然可见那么肯定会执行该item的onBindViewHolder(),执行该方法即会判断是否需要执行加载更多,显然这时是符合条件的,于是就会出现数据未满一屏幕会自动回调onLoadMoreRequested()并且还在那里显示正在加载中.
明显,这时不符合我们的需求的.于是官方有一个解决方案.往下看.
/**
* bind recyclerView {@link #bindToRecyclerView(RecyclerView)} before use!
*
* @see #disableLoadMoreIfNotFullPage(RecyclerView)
*/
public void disableLoadMoreIfNotFullPage() {
//检查当前RecyclerView是否为null
checkNotNull();
disableLoadMoreIfNotFullPage(getRecyclerView());
}
/**
* check if full page after {@link #setNewData(List)}, if full, it will enable load more again.
*
* 不是配置项!!
*
* 这个方法是用来检查是否满一屏的,所以只推荐在 {@link #setNewData(List)} 之后使用
* 原理:先关闭 load more,检查完了再决定是否开启
* 数据项个数未满一屏幕,则不开启load more
* 数据项个数 > 一屏幕,则继续开启load more
*
* 不是配置项!!
*
* @param recyclerView your recyclerView
* @see #setNewData(List)
*/
public void disableLoadMoreIfNotFullPage(RecyclerView recyclerView) {
// 设置加载状态为false
setEnableLoadMore(false);
if (recyclerView == null) return;
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager == null) return;
if (manager instanceof LinearLayoutManager) {
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) manager;
recyclerView.postDelayed(new Runnable() {
@Override
public void run() {
//数据项个数 > 一屏幕,则继续开启load more
if ((linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1) !=
getItemCount()) {
setEnableLoadMore(true);
}
}
}, 50);
} else if (manager instanceof StaggeredGridLayoutManager) {
final StaggeredGridLayoutManager staggeredGridLayoutManager =
(StaggeredGridLayoutManager) manager;
recyclerView.postDelayed(new Runnable() {
@Override
public void run() {
//返回StaggeredGridLayoutManager布局的跨度数
final int[] positions = new int[staggeredGridLayoutManager.getSpanCount()];
//返回每一个跨度(列)的最后一个可见的item的位置 赋值到该数组里面
staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(positions);
//找出数组中最大的数(即StaggeredGridLayoutManager布局的当前可见的最下面那个item)
int pos = getTheBiggestNumber(positions) + 1;
// 数据项个数 > 一屏幕,则继续开启load more
if (pos != getItemCount()) {
setEnableLoadMore(true);
}
}
}, 50);
}
}
/**
* 返回数组中的最大值
* @param numbers
* @return
*/
private int getTheBiggestNumber(int[] numbers) {
int tmp = -1;
if (numbers == null || numbers.length == 0) {
return tmp;
}
for (int num : numbers) {
if (num > tmp) {
tmp = num;
}
}
return tmp;
}
/**
* Set the enabled state of load more.
* 设置上拉加载更多是否可用
*
* @param enable True if load more is enabled, false otherwise.
*/
public void setEnableLoadMore(boolean enable) {
//之前的状态需要和现在的状态做对比
int oldLoadMoreCount = getLoadMoreViewCount();
mLoadMoreEnable = enable;
int newLoadMoreCount = getLoadMoreViewCount();
if (oldLoadMoreCount == 1) {
if (newLoadMoreCount == 0) {
//之前有 现在没有 需要移除
notifyItemRemoved(getLoadMoreViewPosition());
}
} else {
if (newLoadMoreCount == 1) {
//将加载布局插入
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
notifyItemInserted(getLoadMoreViewPosition());
}
}
}
这段代码我看到在开源项目的讨论区异常热门,好像很多人都遇到了使用disableLoadMoreIfNotFullPage()无效的事件.
可能是他们用错了吧,可能.disableLoadMoreIfNotFullPage()是需要在setNewData()之后调用才有效.
disableLoadMoreIfNotFullPage()里面想做的事情就是:判断是否需要load more,他判断的依据是:
查看当前屏幕内的最底部的那个item的索引是否与总的数据项个数相等.
- 如果相等,那么说明未满一屏幕,不需要开启load more
- 如果不相等,那么说明满了一屏幕,需要开启laod more
@Override
public K onCreateViewHolder(ViewGroup parent, int viewType) {
K baseViewHolder = null;
this.mContext = parent.getContext();
this.mLayoutInflater = LayoutInflater.from(mContext);
switch (viewType) {
case LOADING_VIEW:
baseViewHolder = getLoadingView(parent);
break;
case HEADER_VIEW:
baseViewHolder = createBaseViewHolder(mHeaderLayout);
break;
case EMPTY_VIEW:
baseViewHolder = createBaseViewHolder(mEmptyLayout);
break;
case FOOTER_VIEW:
baseViewHolder = createBaseViewHolder(mFooterLayout);
break;
default:
baseViewHolder = onCreateDefViewHolder(parent, viewType);
bindViewClickListener(baseViewHolder);
}
baseViewHolder.setAdapter(this);
return baseViewHolder;
}
private K getLoadingView(ViewGroup parent) {
//加载 加载布局
View view = getItemView(mLoadMoreView.getLayoutId(), parent);
//生成baseviewholder
K holder = createBaseViewHolder(view);
//设置加载布局的点击事件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_FAIL) {
//之前是加载失败状态时 前去刷新
notifyLoadMoreToLoading();
}
if (mEnableLoadMoreEndClick && mLoadMoreView.getLoadMoreStatus() == LoadMoreView
.STATUS_END) {
//加载更多布局可以被点击 并且 之前状态是结束状态
notifyLoadMoreToLoading();
}
}
});
return holder;
}
/**
* The notification starts the callback and loads more
* 通知启动回调并加载更多
*/
public void notifyLoadMoreToLoading() {
//如果当前正在加载中,则不用管
if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_LOADING) {
return;
}
//将加载更多布局的状态设置为默认状态 这样当下面刷新adapter时会回调onBindViewHolder()从而触发
//autoLoadMore()方法去判断是否需要加载更多,这时候刚好又是默认状态是可以更新的,于是就去回调onLoadMoreRequested()方法
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
notifyItemChanged(getLoadMoreViewPosition());
}
这里的目标是给加载更多布局设置点击事件,可以看到其实在代码里面把加载更多布局是直接设置了点击事件的,只是根据不同的状态决定是否需要执行加载更多的逻辑.只有下面2种情况需要去加载更多.
- 之前是加载失败状态时 加载布局被点击
- 之前是结束状态 并且 加载更多布局可以被点击
满足这两种情况时,就把加载布局view的状态设置成默认状态,并且刷新adapter的最后一项(即加载更多布局那一项),这样adapter会回调onBindViewHolder(),而在onBindViewHolder()又调用了autoLoadMore()方法去判断是否需要加载更多,
显然此时是符合条件的,需要刷新,于是回调onLoadMoreRequested(),并且把加载布局的状态改为STATUS_LOADING正在加载的状态,这样加载布局的样式也跟着改变了.
注意不是加载结束,而是本次数据加载结束并且还有下页数据
/**
* Refresh complete
* 刷新完成时调用
*
*/
public void loadMoreComplete() {
if (getLoadMoreViewCount() == 0) {
return;
}
//将当前加载状态改为false 表示未在加载
mLoading = false;
//可进行下一页加载
mNextLoadEnable = true;
// 恢复加载更多布局的状态
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
// 告知加载更多布局被更新了,需要刷新一下
notifyItemChanged(getLoadMoreViewPosition());
}
/**
* Gets to load more locations
* 获取加载更多的布局的索引
* @return
*/
public int getLoadMoreViewPosition() {
return getHeaderLayoutCount() + mData.size() + getFooterLayoutCount();
}
刷新完成之后,需要做一些善后操作,如上所示,代码注释已经很清楚了.
/**
* Refresh failed
* 加载失败
*/
public void loadMoreFail() {
if (getLoadMoreViewCount() == 0) {
return;
}
//当前加载状态 切换为未在加载中
mLoading = false;
//加载布局设置为加载失败
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_FAIL);
//通知加载布局更新了,需要刷新
notifyItemChanged(getLoadMoreViewPosition());
}
就是简单地做一下判断,是否可以继续加载,并且更新布局.
/**
* Refresh end, no more data
* 加载更多,并且没有更多数据了 调用此方法即表示无更多数据了
* 这里设置加载更多布局依然可见
*/
public void loadMoreEnd() {
loadMoreEnd(false);
}
/**
* Refresh end, no more data
* 加载更多,并且没有更多数据了 调用此方法即表示无更多数据了
* gone:设置加载更多布局是否可见 true:不可见 false:可见
* @param gone if true gone the load more view
*/
public void loadMoreEnd(boolean gone) {
if (getLoadMoreViewCount() == 0) {
return;
}
当前加载状态 切换为未在加载中
mLoading = false;
//不能再加载下一页了 因为已经没有更多数据了
mNextLoadEnable = false;
//设置加载更多布局是否可见
mLoadMoreView.setLoadMoreEndGone(gone);
if (gone) {
//如果布局不可见,则更新
notifyItemRemoved(getLoadMoreViewPosition());
} else {
//如果布局可见,则先更新布局(切换为STATUS_END状态那种布局)
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_END);
//并更新adapter
notifyItemChanged(getLoadMoreViewPosition());
}
}
设置加载结束,即表示没有更多的数据可以加载了,于是把mNextLoadEnable标志位设为false,表示无法再加载下一页.
然后根据是否需要显示加载布局,进行刷新adapter.
在源码里面有一个抽象类LoadMoreView.
public abstract class LoadMoreView {
public static final int STATUS_DEFAULT = 1;
/**
* 加载中
*/
public static final int STATUS_LOADING = 2;
/**
* 加载失败
*/
public static final int STATUS_FAIL = 3;
/**
* 加载结束 没有更多数据
*/
public static final int STATUS_END = 4;
/**
* 当前加载更多的状态
*/
private int mLoadMoreStatus = STATUS_DEFAULT;
private boolean mLoadMoreEndGone = false;
public void setLoadMoreStatus(int loadMoreStatus) {
this.mLoadMoreStatus = loadMoreStatus;
}
public int getLoadMoreStatus() {
return mLoadMoreStatus;
}
public void convert(BaseViewHolder holder) {
//根据不同的状态
switch (mLoadMoreStatus) {
case STATUS_LOADING:
visibleLoading(holder, true);
visibleLoadFail(holder, false);
visibleLoadEnd(holder, false);
break;
case STATUS_FAIL:
visibleLoading(holder, false);
visibleLoadFail(holder, true);
visibleLoadEnd(holder, false);
break;
case STATUS_END:
visibleLoading(holder, false);
visibleLoadFail(holder, false);
visibleLoadEnd(holder, true);
break;
case STATUS_DEFAULT:
visibleLoading(holder, false);
visibleLoadFail(holder, false);
visibleLoadEnd(holder, false);
break;
}
}
private void visibleLoading(BaseViewHolder holder, boolean visible) {
holder.setVisible(getLoadingViewId(), visible);
}
private void visibleLoadFail(BaseViewHolder holder, boolean visible) {
holder.setVisible(getLoadFailViewId(), visible);
}
private void visibleLoadEnd(BaseViewHolder holder, boolean visible) {
final int loadEndViewId = getLoadEndViewId();
if (loadEndViewId != 0) {
holder.setVisible(loadEndViewId, visible);
}
}
/**
* 设置标志 有无更多数据
* @param loadMoreEndGone true:无更多数据
*/
public final void setLoadMoreEndGone(boolean loadMoreEndGone) {
this.mLoadMoreEndGone = loadMoreEndGone;
}
public final boolean isLoadEndMoreGone() {
if (getLoadEndViewId() == 0) {
return true;
}
return mLoadMoreEndGone;
}
/**
* No more data is hidden
*
* @return true for no more data hidden load more
* @deprecated Use {@link BaseQuickAdapter#loadMoreEnd(boolean)} instead.
*/
@Deprecated
public boolean isLoadEndGone() {
return mLoadMoreEndGone;
}
/**
* load more layout
*
* @return
*/
public abstract
@LayoutRes
int getLayoutId();
/**
* loading view
*
* @return
*/
protected abstract
@IdRes
int getLoadingViewId();
/**
* load fail view
*
* @return
*/
protected abstract
@IdRes
int getLoadFailViewId();
/**
* load end view, you can return 0
*
* @return
*/
protected abstract
@IdRes
int getLoadEndViewId();
}
该类是用于管理加载布局的,不同的状态显示不同的布局.
源码里面已经给我们提供了一个默认的加载布局,可以直接使用,当然了,是支持自定义的,只需要继承LoadMoreView就行.
默认的加载布局如下:
public final class SimpleLoadMoreView extends LoadMoreView {
@Override
public int getLayoutId() {
return R.layout.quick_view_load_more;
}
@Override
protected int getLoadingViewId() {
return R.id.load_more_loading_view;
}
@Override
protected int getLoadFailViewId() {
return R.id.load_more_load_fail_view;
}
@Override
protected int getLoadEndViewId() {
return R.id.load_more_load_end_view;
}
}
下面是xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40">
<LinearLayout
android:id="@+id/load_more_loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleSmall"
android:layout_marginRight="@dimen/dp_4"/>
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_4"
android:text="@string/loading"
android:textColor="@android:color/black"
android:textSize="@dimen/sp_14"/>
LinearLayout>
<FrameLayout
android:id="@+id/load_more_load_fail_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:id="@+id/tv_prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/load_failed"/>
FrameLayout>
<FrameLayout
android:id="@+id/load_more_load_end_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/load_end"
android:textColor="@android:color/darker_gray"/>
FrameLayout>
FrameLayout>
其实就是一个布局,根据根据状态,动态的显示和隐藏某一种容器就行.
这一块,感觉稍微复杂一些,用了2天的琐碎时间才看完,可能是因为比较菜吧.
大体实现思路就是当检测到滑动到RecyclerView的最后倒数N项时,就开始去刷新,并显示加载布局和回调接口,让外部去实现刷新.
道理虽然很简单,但是实现起来的话,有很多很多细节在里面,很多很多的坑,再次感谢开源库的作者们.