随着Android系统的进步,我们不该老有老旧的ListView来封装下拉刷新,上拉加载了,今天记录一下如何用RecyclerView+SwipeRefreshLayou 封装下拉刷新上拉加载。
Android很奇怪。。以前是没有提供瀑布流,刷新控件等。现在是提供了刷新的SwipeRefreshLayou 但又没提供加载更多的功能╮(╯▽╰)╭。总是要劳烦大家自己弄出五花八门的东西来。而且这个SwipeRefreshLayou 和RecyclerView封装下拉刷新还会出现一些问题。比如:
1、手动调用SwipeRefreshLayou 的setRefresh(true)方法,是可以显示刷新动画,但并不会回调onRefresh()方法。
2、假定已经封装好了,写一个方法供外部调用,内部手动调用setRefresh(true)并且手动调用onRefresh()方法,其他情况都好用,但在首次进入页面的时候,会发现刷新动画不显示。
3、我们实现加载更多的思路是:当滚动到最后一个Item的时候,再就可以加载更多了。但是RecyclerView的布局管理器如何获取到这个情况呢?
4、瀑布流不规则,如何在最后一行显示一个充满屏幕宽度的加载更多布局?
先来看一下效果:
用户体验非常的好,这只是简单的演示版本,成品程序更完善。
这里来贴一下主要实现类的代码:
RefreshLoadingRecyclerView.java 封装SwipeRefreshLayout与RecyclerView的类,以及主要逻辑:
/** * Created by Ade-rui on 2016/12/19. */ public class RefreshLoadingRecyclerView extends FrameLayout implements SwipeRefreshLayout.OnRefreshListener{ /** * 空闲状态 */ public static final int STATE_IDLE = 1002; /** * 加载更多中 */ public static final int STATE_LOAD_MORE = 1003; /** * 刷新中 */ public static final int STATE_REFRESH = 1004; /** * 刷新器 */ private SwipeRefreshLayout swipeRefreshLayout; /** * recyclerview */ private RecyclerView recyclerView; /** * 刷新与加载的回调监听 */ private OnRefreshLoadingListener listener; /** * 布局管理器 */ private ILayoutManager layoutManager; /** * 适配器 */ private BaseRecyclerAdapter adapter; /** * 当前状态 * 默认为空闲 */ private int mCurrentState = STATE_IDLE; /** * 是否开启加载更多功能 */ private boolean isEnableLoadMore; public RefreshLoadingRecyclerView(Context context) { super(context); init(); } public RefreshLoadingRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { LayoutInflater.from(getContext()).inflate(R.layout.layout_recycler_view,this,true); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout); recyclerView = (RecyclerView) findViewById(R.id.recycler_view); swipeRefreshLayout.setOnRefreshListener(this); //设置recyclerView默认的LayoutManager -- > 相当于普通listview样式 this.layoutManager = new MyLinearLayoutManager(getContext()); recyclerView.setLayoutManager(layoutManager.getLayoutManager()); recyclerView.addOnScrollListener(onScrollListener); } /** * 滚动监听 */ private RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); /** * mCurrentState == STATE_IDLE 因为如果是加载中的话,继续有下拉的动作,还会回调加载 * ,所以必须是空闲状态下才能回调加载。进入加载回调就立刻将状态修改为加载状态,避免再次回聊。 * isEnableLoadMore : 用来手动设置是否开启加载更多的功能 * loadingSecurity() :判断是否可以加载更多,如何实现由具体的布局管理器而定 * dy>0 : 为了返回有时候数据不满一屏,都没有滚动,便发生了加载的情况 */ if(mCurrentState == STATE_IDLE&&isEnableLoadMore && loadingSecurity() && dy > 0){ loadMore(); } } }; /** * 设置是否开启加载更多的功能 * @param isEnableLoadMore */ public void isEnableLoadMore(boolean isEnableLoadMore){ this.isEnableLoadMore = isEnableLoadMore; } /** * 加载更多 */ private void loadMore() { if(listener == null){ return; } //修改状态 mCurrentState = STATE_LOAD_MORE; //禁止swipeRefreshLayout功能 swipeRefreshLayout.setEnabled(false); //通知adapter显示加载更多的布局 adapter.loadMoreChangeState(true); listener.onLoading(); } /** * 完成加载或者刷新 * 由用户调用 */ public void complete(){ if(mCurrentState == STATE_LOAD_MORE){ //通知adapter隐藏加载更多布局 adapter.loadMoreChangeState(false); //回复SwipeRefreshLayout功能 swipeRefreshLayout.setEnabled(true); }else if(mCurrentState == STATE_REFRESH){ //让刷新动画消失 swipeRefreshLayout.setRefreshing(false); } mCurrentState = STATE_IDLE; } /** * 是否可以加载 * 根据具体的LayoutManager实现来判断 * @return */ private boolean loadingSecurity() { return layoutManager.loadMore(); } /** * 设置适配器 * @param adapter */ public void setAdapter(BaseRecyclerAdapter adapter){ this.adapter = adapter; recyclerView.setAdapter(adapter); } /** * 设置布局管理器 * @param layoutManager */ public void setLayoutManager(ILayoutManager layoutManager){ this.layoutManager = layoutManager; recyclerView.setLayoutManager(layoutManager.getLayoutManager()); } /** * 设置刷新加载监听 * @param listener */ public void setOnRefreshLoadingListener(OnRefreshLoadingListener listener){ this.listener = listener; } /** * 刷新 */ public void setRefresh(){ /** * 使用post加入消息队列调用---》 可以防止第一次进入页面的时候, * 虽然执行swipeRefreshLayout.setRefreshing(true),但并不会显示刷新动画的bug */ post(new Runnable() { @Override public void run() { swipeRefreshLayout.setRefreshing(true); onRefresh(); } }); } /** * RereshLayout监听到下拉手势的回调,然后我们在这个回调里面修改自身控件的状态 * 并且回调总结的listener给用户 */ @Override public void onRefresh() { if(listener!=null){ mCurrentState = STATE_REFRESH; Log.i("mydata","onRefresh"); listener.onRefresh(); } } /** * 监听回调 */ public interface OnRefreshLoadingListener{ void onRefresh(); void onLoading(); } }
短短的200行代码便可以实现这个功能了。在XML文件声明以及在Activity中的使用:
Activity:
pullView = (RefreshLoadingRecyclerView) findViewById(R.id.pull_view); adapter = new BaseRecyclerAdapter(new ArrayList()); pullView.setAdapter(adapter); pullView.setOnRefreshLoadingListener(new RefreshLoadingRecyclerView.OnRefreshLoadingListener() { @Override public void onRefresh() { refresh(); } @Override public void onLoading() { load(); } }); pullView.isEnableLoadMore(true); pullView.setRefresh();
使用起来与流行的第三方库等没有任何差别。
现在来统一总结一下都需要解决哪些问题:
1、手动调用SwipeRefreshLayou 的setRefresh(true)方法,是可以显示刷新动画,但并不会回调onRefresh()方法。
解决方法与2是一体的,向外提供一个setRefresh()方法,外部调用的时候,内部同时调用显示动画和onRefresh方法。
2、假定已经封装好了,写一个方法供外部调用,内部手动调用setRefresh(true)并且手动调用onRefresh()方法,其他情况都好用,但在首次进入页面的时候,会发现刷新动画不显示。
在内部使用post加入消息队列中使用,在第一次进入页面就也可以看到刷新动画了
post(new Runnable() {
@Override
public void run() {
swipeRefreshLayout.setRefreshing(true);
onRefresh();
}
});
3、我们实现加载更多的思路是:当滚动到最后一个Item的时候,再就可以加载更多了。但是RecyclerView的布局管理器如何获取到这个情况呢?
LinearLayoutManager和GridLayoutManager的方法:
//最后一个可见Item的索引 int lastVisiableItemIndex = findLastVisibleItemPosition(); //总共Item的数量 int totalItemIndex = getItemCount(); //这里取<=1的时候返回true表示可以加载更多,也就是说当滑动到倒数第二个Item出现的时候就代表可以加载了
这两个布局管理器都有获取最后一个课件Item的方法,但是StaggeredGridLayoutManager 没有这个方法,用以前方法获取:
int[] positions = null; positions = findLastVisibleItemPositions(positions); positions[0];
因为瀑布流一行中不等高,所以从概念上来讲,就是得到一组最后出现的Item索引,我们拿到第一个进行比较就行了,不会差很多的。
4、瀑布流不规则,GridLayoutManager一列不占全屏幕,那么如何在最后一行显示一个充满屏幕宽度的加载更多布局?
一、GridLayoutManager的解决方案:
GrildLayoutManager源码对于类的解释大概有如下:
/**
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
*
* By default, each item occupies 1 span. You can change it by providing a custom
* {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
*/
大致意思就是,每一列显示1span单位, 如果你想改变这一列所占的单位span 就调用setSpanSizeLookup方法,那么span是什么?是我们在new GridLayoutManager时传入的第二个参数就是spanCount,几列就传入几。所以重写GrildLayoutManager:
/**
* Created by lenovo on 2016/12/20.
*/
public class MyGridLayoutManager extends GridLayoutManager implements ILayoutManager {
public MyGridLayoutManager(BaseRecyclerAdapter adapter,Context context, int spanCount) {
super(context, spanCount);
setSpanSizeLookup(new BaseRecyclerAdapter.FooterSpanSize(adapter,this));
}
public MyGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
super(context, spanCount, orientation, reverseLayout);
}
@Override
public boolean loadMore() {
//第一个可见Item的索引
int lastVisiableItemIndex = findLastVisibleItemPosition();
//总共Item的数量
int totalItemIndex = getItemCount();
//这里取<=1的时候返回true表示可以加载更多,也就是说当滑动到倒数第二个Item出现的时候就代表可以加载了
//你也可以写成<5 或者 == 0 随便,由自己的体验来决定
return totalItemIndex - lastVisiableItemIndex <= 1;
}
@Override
public RecyclerView.LayoutManager getLayoutManager() {
return this;
}
@Override
public SpanSizeLookup getSpanSizeLookup() {
return super.getSpanSizeLookup();
}
}
在构造的时候便调用setSpanSizeLookUp设置我们自定义的lookUpSize:
public static class FooterSpanSize extends GridLayoutManager.SpanSizeLookup {
private BaseRecyclerAdapter adapter;
private GridLayoutManager layoutManager;
public FooterSpanSize(BaseRecyclerAdapter adapter,GridLayoutManager layoutManager){
this.layoutManager = layoutManager;
this.adapter = adapter;
}
/**
* 每显示一个Item的时候这个方法就会被回调,并传入item的索引position;
* 为什么要持有Adapater的引用? 因为我们需要让Adapter来判断是否是加载更多的布局
* 为什么要持有自身LayoutManager的引用? 因为我们需要知道总共几列
* @param position
* @return
*/
@Override
public int getSpanSize(int position) {
if(adapter.enableLoadMore(position)){
return layoutManager.getSpanCount();
}
return 1;
}
}
那么StaggeredGridLayoutManager是否也有这样的方法? 答案,没有,那么该如何让加载更多的Item显示整行呢?
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//如果是最后一个Item并且要加载更多,则只显示加载更多的布局,不进行bind数据操作
if(position == getItemCount() - 1 && isLoadMore){
if(holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams){
StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
params.setFullSpan(true);
}
return;
}
//这里其实应该写一个BaseViewHolder 调用其中的onBind抽象方法,让其具体的子类去绑定
//不要用强转这种方式,这里为了做例子方便所以强转
((ContentViewHolder)holder).tvContent.setText(datas.get(position));
}
我们只有在onBind的时候来动态判断,此viewHolder的布局的LayoutParams是不是StaggeredGridLayoutManager.LayoutParams 如果是的话就取出来,调用去setFullSpan(true),来让此Item显示一整行。有人说为什么不在ViewHolder初始化的时候就调用,我只想说。。那时候你的View还没有添加进入瀑布流的父View,自身的LayoutParams应该还不是为StaggeredGridLayoutManager.LayoutParams的吧。个人臆测,没有实际测验。
为什么要有ILayoutManager接口?
因为每一个layoutManager判断是否可以加载更多的逻辑不一样,所以必须用接口来编程。
文章中填出的代码示例直接拷贝不能立即使用,想使用请下载源码查看修改自己对应需求来使用就行。
后期我会将此Demo整理成可用项目上传至GitHub
---------------------------------------------------------------
所有的难点都已经解决,代码敲得灵活度并不是很大,不适合开源,但对应每一个想为自己项目封装一套的童鞋来说,这个完全没有问题,覆盖了所有的解决的方案。如果有什么问题,请留言。
源码链接-------http://download.csdn.net/detail/sinat_31311947/9717971