前言:大家都在更青睐于使用RecyclerVIew来替代ListView,但是在使用的时候我们会发现ListView的一些常用方法在RecyclerView中没有,比如添加头部、尾部。而且在刷新加载方面ListView的封装也比较多,如我之前常用的PullToRefreshListView,之前也在PullToRefresh项目上做过支持RecyclerView,但是效果还是不能让人满意。于是想写一个好用的RecyclerView来方便开发中使用。
一、 最终效果

二、 需求
我们希望做可以做成这样的,可以刷新加载,可以添加头部尾部。并且在使用的时候要贴近ListView的用法,不要让使用者去学习它怎么去使用。
三、需求分析
根据以上,刷新加载选取PullToRefresh为基础以及指导思想。为什么使用PullToRefresh?站在巨人的肩膀上,而且对它的源码比较了解,没有看过我之前博客的朋友可以看下 《 Android PullToRefresh 完全解析》,这里分为了五篇博客来从原理上详细介绍PullToRefresh框架。
看过《Android PullToRefresh 分析之四、扩展RecyclerView》的朋友知道之前已经在PullToRefresh上做了简单的支持RecyclerView,有些朋友在使用的时候提出了一些问题,比如在刷新加载的时候RecyclerView不能滚动,这个问题要想解决会比较复杂,扩展RecyclerView,使其支持添加头部尾部。于是在这篇博客中《Android RecyclerView添加头部和尾部》介绍了封装的可以添加头部尾部的RecyclerView。
四、实现思路
所以就导致了头部或者尾部显示的时候不能去让中间的内容区域滚动,有的朋友就会说那PullToRefreshListView是如何实现的呢,它的实现就是在有两个刷新头部和两个加载尾部,听起来不可思议,由于ListView可以添加头部尾部,就在ListView的头部添加一个刷新头部,在ListView的尾部添加一个刷新尾部,平常的时候是隐藏的,只有刷新加载动作促发的时候,将其显示,并把原来的头部隐藏。
在正常显示到开始下拉,将要促发刷新状态,这些过程中添加到ListView头部的刷新头部都是隐藏的。
在加载数据的时候,原来的头部隐藏,添加到ListView头部的加载提示View显示。
总结一下状态的切换,如下图所示:
分析了那么多PullToRefreshListView,我们的RecyclerView也可以这么实现的。
五、实现步骤
1. 继承PullToRefreshBase,实现抽象方法,生成构造
public class UltimateRecyclerView extends PullToRefreshBase<WrapRecyclerView> {
public UltimateRecyclerView(Context context) {
super(context);
}
public UltimateRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public UltimateRecyclerView(Context context, Mode mode) {
super(context, mode);
}
public UltimateRecyclerView(Context context, Mode mode, AnimationStyle animStyle) {
super(context, mode, animStyle);
}
@Override
public Orientation getPullToRefreshScrollDirection() {
return null;
}
@Override
protected WrapRecyclerView createRefreshableView(Context context, AttributeSet attrs) {
return null;
}
@Override
protected boolean isReadyForPullEnd() {
return false;
}
@Override
protected boolean isReadyForPullStart() {
return false;
}
}
2. 设置刷新方向为竖向
@Override
public final Orientation getPullToRefreshScrollDirection() {
return Orientation.VERTICAL;
}
3. 设置刷新的View
这里是封装的可以添加头部尾部的 WarpRecyclerView
@Override
protected WrapRecyclerView createRefreshableView(Context context, AttributeSet attrs) {
WrapRecyclerView recyclerView = new InternalWrapRecyclerView(context, attrs);
recyclerView.setId(R.id.ultimate_recycler_view);
return recyclerView;
}
这里为什么使用了一个InternalWrapRecyclerView,这个后面再讲。
4. 设置判断是否到顶部
@Override
protected boolean isReadyForPullStart() {
return isFirstItemVisible();
}
5. 设置判断是否到底部
@Override
protected boolean isReadyForPullEnd() {
return isLastItemVisible();
}
6. 在RecyclerView添加刷新头部和初始化加载尾部
private void init(Context context, AttributeSet attrs) {
// Styleables from XML
TypedArray ua = context.obtainStyledAttributes(attrs, R.styleable.UltimateRecyclerView);
mURecyclerViewExtrasEnabled = ua.getBoolean(R.styleable.UltimateRecyclerView_ptrURecyclerViewExtrasEnabled, true);
ua.recycle();
// Styleables from XML
TypedArray pa = context.obtainStyledAttributes(attrs, com.handmark.pulltorefresh.library.R.styleable.PullToRefresh);
if (mURecyclerViewExtrasEnabled) {
final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
final ViewGroup.LayoutParams hlp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
// Create Loading Views ready for use later
mSvHeaderLoadingFrame = new FrameLayout(getContext());
mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, pa);
mHeaderLoadingView.setVisibility(View.GONE);
mSvHeaderLoadingFrame.addView(mHeaderLoadingView, lp);
mSvHeaderLoadingFrame.setLayoutParams(hlp);
mRefreshableView.addHeaderView(mSvHeaderLoadingFrame);
mSvFooterLoadingFrame = new FrameLayout(getContext());
mFooterLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_END, pa);
mFooterLoadingView.setVisibility(View.GONE);
mSvFooterLoadingFrame.addView(mFooterLoadingView, lp);
mSvFooterLoadingFrame.setLayoutParams(hlp);
mSvSecondFooterLoadingFrame = new FrameLayout(getContext());
mSvSecondFooterLoadingFrame.setLayoutParams(hlp);
pa.recycle();
}
}
细心的朋友会发现,这里只有通过
mRefreshableView.addHeaderView(mSvHeaderLoadingFrame);添加了刷新头部到RecyclerView,并没有添加加载尾部到RecyclerView1呀,对的。是因为在没有设置RecyclerView的Adapter之前我们不希望加载尾部会出现,因为这时候没有意义。在上文提到的InternalWrapRecyclerView中去设置;
7. 在RecyclerView添加加载尾部
protected class InternalWrapRecyclerView extends WrapRecyclerView {
private boolean mAddedSvFooter = false;
public InternalWrapRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void setAdapter(Adapter adapter) {
// Add the Footer View at the last possible moment
if (null != mSvFooterLoadingFrame && !mAddedSvFooter) {
addFooterView(mSvFooterLoadingFrame);
mAddedSvFooter = true;
}
super.setAdapter(adapter);
}
}
8. 在刷新的时候隐藏原来的头部尾部,显示添加到RecyclerView的头部尾部
@Override
protected void onRefreshing(final boolean doScroll) {
WrapAdapter adapter = mRefreshableView.getAdapter();
if (!mURecyclerViewExtrasEnabled || !getShowViewWhileRefreshing() || null == adapter || adapter.getItemCount() == 0) {
super.onRefreshing(doScroll);
return;
}
super.onRefreshing(false);
final LoadingLayoutBase origLoadingView, recyclerViewLoadingView, oppositeRecyclerViewLoadingView;
final int scrollToPosition, scrollToY;
switch (getCurrentMode()) {
case MANUAL_REFRESH_ONLY:
case PULL_FROM_END:
origLoadingView = getFooterLayout();
recyclerViewLoadingView = mFooterLoadingView;
oppositeRecyclerViewLoadingView = mHeaderLoadingView;
scrollToPosition = mRefreshableView.getBottom();
scrollToY = getScrollY() - getFooterSize();
break;
case PULL_FROM_START:
default:
origLoadingView = getHeaderLayout();
recyclerViewLoadingView = mHeaderLoadingView;
oppositeRecyclerViewLoadingView = mFooterLoadingView;
scrollToPosition = mRefreshableView.getTop();
scrollToY = getScrollY() + getHeaderSize();
break;
}
// 隐藏原来的加载View
origLoadingView.reset();
origLoadingView.hideAllViews();
// 刷新时隐藏尾部,加载时隐藏头部
oppositeRecyclerViewLoadingView.setVisibility(View.GONE);
// 设置RecyclerView内的加载View显示并设置它为刷新状态
recyclerViewLoadingView.setVisibility(View.VISIBLE);
recyclerViewLoadingView.refreshing();
if (doScroll) {
// We need to disable the automatic visibility changes for now
disableLoadingLayoutVisibilityChanges();
// 刷新布局由过度滑动状态恢复
setHeaderScroll(scrollToY);
// 让添加到RecyclerView的刷新头部或者加载尾部显示出来
mRefreshableView.smoothScrollToPosition(scrollToPosition);
// 把整体滚回初始位置
smoothScrollTo(0);
}
}
通过注释就可以了解执行的过程,在刷新加载数据的时候,我们添加到RecyclerView的就显示出来接管了原来的刷新加载布局。
9. 刷新加载完成恢复状态
@Override
protected void onReset() {
if (!mURecyclerViewExtrasEnabled) {
super.onReset();
return;
}
final LoadingLayoutBase originalLoadingLayout, recyclerViewLoadingLayout;
final int scrollToHeight, selection;
final boolean scrollSvToEdge;
WrapAdapter adapter = mRefreshableView.getAdapter();
switch (getCurrentMode()) {
case MANUAL_REFRESH_ONLY:
case PULL_FROM_END:
originalLoadingLayout = getFooterLayout();
recyclerViewLoadingLayout = mFooterLoadingView;
selection = adapter.getItemCount() - 1;
scrollToHeight = getFooterSize();
scrollSvToEdge = Math.abs(getLastVisiblePosition() - selection) <= 1;
break;
case PULL_FROM_START:
default:
originalLoadingLayout = getHeaderLayout();
recyclerViewLoadingLayout = mHeaderLoadingView;
scrollToHeight = -getHeaderSize();
selection = 0;
scrollSvToEdge = Math.abs(getFirstVisiblePosition() - selection) <= 1;
break;
}
// 如果添加到RecyclerView的加载布局在显示
if (recyclerViewLoadingLayout.getVisibility() == View.VISIBLE) {
// 显示原来的加载布局
originalLoadingLayout.showInvisibleViews();
// 隐藏添加到RecyclerView的加载布局
recyclerViewLoadingLayout.setVisibility(View.GONE);
// 滚动隐藏头部或者尾部
if (scrollSvToEdge && getState() != State.MANUAL_REFRESHING) {
mRefreshableView.scrollToPosition(selection);
setHeaderScroll(scrollToHeight);
}
}
super.onReset();
}
重置加载刷新状态,简单说就是收拾刷新加载形成的烂摊子。
OK,通过以上操作就能实现刷新加载的时候也可以滚动RecyclerView啦。
六、优化加载尾部
我们大小好多程序的效果是这样的,到尾部的时候就自动加载,不需要再往上滑一下。这里需要监听是否是滑动到了最后一个。原理也比较简单。
public final void setOnLastItemVisibleListener(OnLastItemVisibleListener listener) {
mOnLastItemVisibleListener = listener;
}
public static interface OnLastItemVisibleListener {
public void onLastItemVisible();
}
是不是干净利索,最简单的一个回调。
重点就是怎么判断到最后一个:
/**
* 判断最后一个条目是否能够可见
*
* @return boolean:
* @version 1.0
* @date 2016-4-12 14:51:04
* @Author zhou.wenkai
*/
private boolean isLastItemVisible() {
final RecyclerView.Adapter<?> adapter = getRefreshableView().getAdapter();
// 如果未设置Adapter,都没有添加自然不可见
if(null == adapter) {
return false;
} else {
// 最后一个条目View是否展示
int lastVisiblePosition = getLastVisiblePosition();
// 最后一个显示出来了
if(lastVisiblePosition == mRefreshableView.getAdapter().getItemCount() - 2) {
// 说明最后一个刚刚显示出来
// 这里不希望和PullToRefreshListView中一样只要最后一个显示,每动一下就促发一次回调
if(lastVisiblePosition == mTmplastVisiblePosition + 1) {
mTmplastVisiblePosition = lastVisiblePosition;
return true;
}
}
mTmplastVisiblePosition = lastVisiblePosition;
}
return false;
}
private int getLastVisiblePosition() {
View lastVisibleChild = mRefreshableView.getChildAt(mRefreshableView
.getChildCount() - 1);
return lastVisibleChild != null ? mRefreshableView
.getChildAdapterPosition(lastVisibleChild) : -1;
}
判断是否最后一个显示的方法有了,那么只要实时的监控就可以啦。监控可以写在 onScroll的回调中:
@Override
public void onScrolled(int dx, int dy) {
super.onScrolled(dx, dy);
boolean lastItemVisible = isLastItemVisible();
if(lastItemVisible) {
mOnLastItemVisibleListener.onLastItemVisible();
}
}
七、添加修改刷新头部、加载尾部
扩展刷新加载样式在《Android PullToRefresh 分析之五、扩展刷新加载样式》有详细介绍原理,这里只是把代码列出来。
1. 设置头部刷新布局
@Override
public void setHeaderLayout(LoadingLayoutBase headerLayout) {
super.setHeaderLayout(headerLayout);
try {
Constructor c = headerLayout.getClass().getDeclaredConstructor(new Class[]{Context.class});
LoadingLayoutBase mHeaderLayout = (LoadingLayoutBase)c.newInstance(new Object[]{getContext()});
if(null != mHeaderLayout) {
mSvHeaderLoadingFrame.removeAllViews();
final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
mHeaderLoadingView = mHeaderLayout;
mHeaderLoadingView.setVisibility(View.GONE);
mSvHeaderLoadingFrame.addView(mHeaderLoadingView, lp);
mRefreshableView.getAdapter().notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
}
}
2. 设置尾部加载布局
@Override
public void setFooterLayout(LoadingLayoutBase footerLayout) {
super.setFooterLayout(footerLayout);
try {
Constructor c = footerLayout.getClass().getDeclaredConstructor(new Class[]{Context.class});
LoadingLayoutBase mFooterLayout = (LoadingLayoutBase)c.newInstance(new Object[]{getContext()});
if(null != mFooterLayout) {
mSvFooterLoadingFrame.removeAllViews();
final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
mFooterLoadingView = mFooterLayout;
mFooterLoadingView.setVisibility(View.GONE);
mSvFooterLoadingFrame.addView(mFooterLoadingView, lp);
mRefreshableView.getAdapter().notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
}
}
自定义的刷新加载布局由于使用了反射创建实例,意义要保障有一个带有Context的构造函数。
八、添加设置尾部加载提示
我们好多时候需要在数据加载完成的时候提示用户没有更多数据啦,有了可以添加尾部的RecyclerView这个就比较简单了。我们只需要再在尾部添加一个View就可以啦。
@Override
public void setAdapter(Adapter adapter) {
// Add the Footer View at the last possible moment
if (null != mSvFooterLoadingFrame && !mAddedSvFooter) {
addFooterView(mSvSecondFooterLoadingFrame);
addFooterView(mSvFooterLoadingFrame);
mAddedSvFooter = true;
}
super.setAdapter(adapter);
}
在原来添加尾部的地方再加一个提示布局,然后提供一个设置布局的方法:
@Override
public void setSecondFooterLayout(View secondFooterLayout) {
final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
mSvSecondFooterLoadingFrame.addView(secondFooterLayout, lp);
}
九、简单使用
1. 在xml中添加控件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ptr="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.kevin.ultimaterecyclerview.UltimateRecyclerView
android:id="@+id/main_act_urv"
android:layout_width="match_parent"
android:layout_height="match_parent"
ptr:ptrMode="pullFromStart">
</com.kevin.ultimaterecyclerview.UltimateRecyclerView>
</RelativeLayout>
2. 代码中初始化
mUltimateRecyclerView = (UltimateRecyclerView) this.findViewById(R.id.main_act_urv);
// 设置头部刷新样式为自定义
mUltimateRecyclerView.setHeaderLayout(new TmallHeaderLayout(this));
3. 获取RecyclerView
WrapRecyclerView mWrapRecyclerView = mUltimateRecyclerView.getRefreshableView();
这里的WrapRecyclerView是对RecyclerView可以添加头部尾部的封装。
4. 添加RecyclerView头部(视需求而定)
LayoutInflater inflater = LayoutInflater.from(this);
FrameLayout layout = (FrameLayout) inflater.inflate(R.layout.recycler_header, null);
mAdLoopView = (AdLoopView) layout.findViewById(R.id.main_act_alv);
mWrapRecyclerView.addHeaderView(layout);
5. 设置刷新监听
// 设置刷新监听
mUltimateRecyclerView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<WrapRecyclerView>() {
@Override
public void onRefresh(PullToRefreshBase<WrapRecyclerView> refreshView) {
new GetDataTask(true).execute();
}
});
// 设置最后一个条目可见监听
mUltimateRecyclerView.setOnLastItemVisibleListener(new PullToRefreshBase.OnLastItemVisibleListener() {
@Override
public void onLastItemVisible() {
boolean hasMoreData = secondFooterLayout.isHasMoreData();
Log.i("", "是否还有更多数据 " + hasMoreData);
if(hasMoreData) {
new GetDataTask(false).execute();
}
}
});
使用和原PullToRefresh框架基本一致,这里就不再赘述,详细的使用请参考本项目的示例,以及以及原项目示例。
十、源码及示例
十一、一行引入库
如果您的项目使用 Gradle 构建, 只需要在您的build.gradle
文件添加下面一行到 dependencies
:
compile 'com.kevin:ultimaterecyclerview:1.0.2'