上拉加载下拉刷新的RecyclerView可添加headerView
这个demo来自 github 我只是对代码重构了一下,支持原作者。
先说一下思路把,上拉和下拉都只是是给RecyclerView添加了一个headerView和footerView。在用listView时添加一个headerView和footerView很简单,只要add一下就可以了。但到RecyclerView上可没有什么add方法那怎么办?
看一下listView是怎么做的,仿着它弄一个。在listView.setAdapter()方法中有这样一段代码
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
是不是一下就明白了!就是说如果你添加了headerView或footerView它都会把你的adapter在封装成另一个HeaderViewListAdapter。
它里面有这样一个方法
public int getItemViewType(int position) {
int numHeaders = getHeadersCount();
if (mAdapter != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getItemViewType(adjPosition);
}
}
return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
}
在用listView显示不同的item时,我们就会在adapter中重写这个方法return不同的type来实现。看HeaderViewListAdapter的这个方法就是说如果有headerView或footerView就会返回AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER否则返回adapter的getItemViewType
好了有了这些基本就可以了,接下来我们自己来实现一个
public class WrapAdapter extends RecyclerView.Adapter{
private static final int TYPE_REFRESH_HEADER = -5;
private static final int TYPE_HEADER = -4;
private static final int TYPE_FOOTER = -3;
private static final int TYPE_NORMAL = 0;
private RecyclerView.Adapter adapter;
private ArrayList<View> mHeaderViews;
private ArrayList<BaseMoreFooter> mFootViews;
private int headerPosition = 1;
public WrapAdapter(ArrayList<View> headerViews, ArrayList<BaseMoreFooter> footViews, RecyclerView.Adapter adapter) {
this.adapter = adapter;
this.mHeaderViews = headerViews;
this.mFootViews = footViews;
}
public boolean isHeader(int position) {
return position >= 0 && position < mHeaderViews.size();
}
public boolean isFooter(int position) {
return position < getItemCount() && position >= getItemCount() - mFootViews.size();
}
public boolean isRefreshHeader(int position) {
return position == 0 ;
}
public int getHeadersCount() {
return mHeaderViews.size();
}
public int getFootersCount() {
return mFootViews.size();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_REFRESH_HEADER) {
return new SimpleViewHolder(mHeaderViews.get(0));
} else if (viewType == TYPE_HEADER) {
return new SimpleViewHolder(mHeaderViews.get(headerPosition++ ));
} else if (viewType == TYPE_FOOTER) {
return new SimpleViewHolder((View) mFootViews.get(0));
}
return adapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (isHeader(position)) {
return;
}
int adjPosition = position - getHeadersCount();
int adapterCount;
if (adapter != null) {
adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
adapter.onBindViewHolder(holder, adjPosition);
}
}
}
@Override
public int getItemCount() {
if (adapter != null) {
return getHeadersCount() + getFootersCount() + adapter.getItemCount();
} else {
return getHeadersCount() + getFootersCount();
}
}
@Override
public int getItemViewType(int position) {
if(isRefreshHeader(position)){
return TYPE_REFRESH_HEADER;
}
if (isHeader(position)) {
return TYPE_HEADER;
}
if(isFooter(position)){
return TYPE_FOOTER;
}
int adjPosition = position - getHeadersCount();
int adapterCount;
if (adapter != null) {
adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
return adapter.getItemViewType(adjPosition);
}
}
return TYPE_NORMAL;
}
@Override
public long getItemId(int position) {
if (adapter != null && position >= getHeadersCount()) {
int adjPosition = position - getHeadersCount();
int adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
return adapter.getItemId(adjPosition);
}
}
return -1;
}
@Override
public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
if (adapter != null) {
adapter.unregisterAdapterDataObserver(observer);
}
}
@Override
public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
if (adapter != null) {
adapter.registerAdapterDataObserver(observer);
}
}
private class SimpleViewHolder extends RecyclerView.ViewHolder {
public SimpleViewHolder(View itemView) {
super(itemView);
}
}
}
这样就可以了,但是这样的话使用RecyclerView时,设置LayoutManager只能是LinearLayoutManager。看一下效果就明白为什么了。
如何解决呢?
如果是GridLayoutManager,重写adapter的onAttachedToRecyclerView
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if(manager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) manager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return (isHeader(position)|| isFooter(position)) ? gridManager.getSpanCount() : 1;
}
});
}
}
解释一下,我们设置了一个SpanSizeLookup,这个类是一个抽象类,而且仅有一个抽象方法getSpanSize,这个方法的返回值决定了我们每个position上的item占据的单元格个数。假如GridLayoutManager设置的每行的个数为2的话,如果当前位置是header的位置,那么该item占据2个单元格,正常情况下占据1个单元格
如果是StaggeredGridLayoutManager,重写adapter的onViewAttachedToWindow方法
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if(lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams
&& (isHeader( holder.getLayoutPosition()) || isFooter( holder.getLayoutPosition())) ) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
StaggeredGridLayoutManager的处理方式是用通过LayoutParams,而且这里更简单,StaggeredGridLayoutManager.LayoutParams为我们提供了一个setFullSpan方法来设置占领全部空间
好了这下就都可以正常显示了
adapter完事了,接下来看一下实现上拉加载下拉刷新的XRecyclerView,它继承RecyclerView。我先把全局变量粘出来,如果不知道是什么意思的话就先往下看吧
private static final float DRAG_RATE = 3;
private boolean hasMore = false;// 还有更多
private boolean pullRefreshEnabled = true;//下拉刷新
private boolean loadingMoreEnabled = true;//上拉加载
private ArrayList<View> mHeaderViews = new ArrayList<>();//头部view的集合
private ArrayList<BaseMoreFooter> mFootViews = new ArrayList<>();//尾部view的集合
private Adapter mAdapter;//里层的adapter
private float mLastY = -1;
private int pageSize = 10;
private int visibleThreshold = 1; // list到达 最后一个item的时候 触发加载
private LoadingListener mLoadingListener;
private ArrowRefreshHeader mRefreshHeader;//header view
额这个类的代码有点长我就把主要的说一下吧!
在每一个构造方法调用一下init()方法
private void init(Context context) {
//添加一个header view 用于下拉刷新
ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(context);
mHeaderViews.add(0, refreshHeader);
mRefreshHeader = refreshHeader;
//添加一个footer view 用于上拉加载
LoadingMoreFooter footView = new LoadingMoreFooter(context);
footView.setLoadingMoreFooterClickCallback(this);
addFootView(footView);
mFootViews.get(0).setViewVisibility(GONE);
}
@Override
public void setAdapter(Adapter adapter) {
mAdapter = adapter;
WrapAdapter wrapAdapter = new WrapAdapter(mHeaderViews, mFootViews, mAdapter);
mAdapter.registerAdapterDataObserver(new AdapterDataObserverImpl(wrapAdapter));
super.setAdapter(wrapAdapter);
}
这个方法set了刚刚写的WrapAdapter。为传进来的adapter注册了一个观察者,如果不设置这个观察者,那么在调用adapter.notifyDataSetChanged()方法不会起任何作用,因为在setAdapter,set的是wrapAdapter而不是传进来的adapter。当然你也可以这样mRecyclerView.getAdapter().notifyDataSetChanged()
下拉加载的触发
@Override
public boolean onTouchEvent(MotionEvent ev) {
public boolean onTouchEvent(MotionEvent ev) {
//下拉刷新的实现
if (pullRefreshEnabled) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
if (isOnTop()) {
mRefreshHeader.onMove(deltaY / DRAG_RATE);
}
break;
default:
mLastY = -1;
if (isOnTop()) {
if (mRefreshHeader.releaseAction()) {
if (mLoadingListener != null) {
mLoadingListener.onRefresh();
hasMore = false;
}
}
}
break;
}
}
return super.onTouchEvent(ev);
}
}
一个onTouchEvent搞定,在拖动recyclerViewd的时候,改变headerView的高度,在松手的时候mRefreshHeader.releaseAction()会判断当前headerView的是否全部显示出来了,返回true的话就调用下拉刷新的回调方法
在说一下上拉加载
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
//上拉加载的实现
if (loadingMoreEnabled) {
BaseMoreFooter footView = mFootViews.get(0);
//如果回调的监听不等与null,并且footView没有在加载中
if (mLoadingListener != null && !footView.isLoading()) {
LayoutManager layoutManager = getLayoutManager();
int lastVisibleItemPosition = getLastVisibleItemPosition(layoutManager);
//item大于0,并且到最后一个item,并且还有更多数据,并且没有在下拉刷新中,
// 并且LoadingMoreFooter的状态不是click加载的状态
if (layoutManager.getChildCount() > 0
&& lastVisibleItemPosition >= layoutManager.getItemCount() - visibleThreshold
&& !hasMore
&& !isRefreshing()
&& !footView.isClickLoadMore()) {
footView.loading();
mLoadingListener.onLoadMore();
}
}
}
}
上拉加载核心方法是getLastVisibleItemPosition
/**
* 返回显示的最后一个view的position
*/
public int getLastVisibleItemPosition(LayoutManager layoutManager){
int lastVisibleItemPosition;
if (layoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
lastVisibleItemPosition = findMax(into);
} else {
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
}
return lastVisibleItemPosition;
}
private int findMax(int[] lastPositions) {
int max = lastPositions[0];
for (int value : lastPositions) {
if (value > max) {
max = value;
}
}
return max;
}
接下来看一下如果使用
private MyAdapter mAdapter;
private ArrayList<String> listData;
private int refreshTime = 0;
private int times = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recyclerview);
mRecyclerView = (XRecyclerView)this.findViewById(R.id.recyclerview);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setArrowImageView(R.drawable.iconfont_downgrey);
View header = LayoutInflater.from(this).inflate(R.layout.recyclerview_header, (ViewGroup)findViewById(android.R.id.content),false);
mRecyclerView.addHeaderView(header);
mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() {
@Override
public void onRefresh() {
refreshTime ++;
times = 0;
new Handler().postDelayed(new Runnable(){
public void run() {
listData.clear();
for(int i = 0; i < 5 ;i++){
listData.add("item" + i + "after " + refreshTime + " times of refresh");
}
mAdapter.notifyDataSetChanged();
mRecyclerView.refreshComplete();
}
}, 1000); //refresh data here
}
@Override
public void onLoadMore() {
if(times < 2){
new Handler().postDelayed(new Runnable(){
public void run() {
mRecyclerView.stopLoadMore();
for(int i = 0; i < 15 ;i++){
listData.add("item" + (i + listData.size()) );
}
mAdapter.notifyDataSetChanged();
mRecyclerView.restoreFooter();
}
}, 1000);
} else {
new Handler().postDelayed(new Runnable() {
public void run() {
mAdapter.notifyDataSetChanged();
mRecyclerView.noMoreLoading();
}
}, 1000);
}
times ++;
}
});
listData = new ArrayList<String>();
for(int i = 0; i < 5 ;i++){
listData.add("item" + (i + listData.size()) );
}
mAdapter = new MyAdapter(listData);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.clickLoadMore();
}
LoadingListener有上拉和下拉的回调,回调里面模仿了一下网络请求。在最后一行我调用的mRecyclerView.clickLoadMore()方法,这个方法是让recyclerView的footerView显示点击加载,因为有这种情况:比如一页的数据不满一屏或网络请求失败都可以显示这个。
下拉刷新完会调用mRecyclerView.refreshComplete(),这个方法没什么可说的,就是刷新完成
上拉加载完可以调用两个方法mRecyclerView.restoreFooter()和mRecyclerView.noMoreLoading(),noMoreLoading()就是显示没有更多了,然后怎么拖动recyclerView都不会进行上拉加载了。restoreFooter()方法看一下代码吧
/**
* 重置footer.如果当前 itemCount > {@link #pageSize} 调用 {@link #stopLoadMore()} 否则调用 {@link #clickLoadMore()}
*/
public void restoreFooter(){
if (loadingMoreEnabled){
int itemCount = mAdapter.getItemCount();
if (itemCount >= pageSize){
stopLoadMore();
}else {
clickLoadMore();
}
}
}
这个一开始我本来是想计算所有item的高度,如果大于一屏就让他自动加载,否则就显示点击加载。但是后来想想,每调用一下这个方法都要for循环一次,不太好,所以就简单一点如果所有item(不包含headerView和footerView)大于pageSize就让他自动加载。
如果有任何意见或建议请@我,我会及时更改。群号–284568173,我叫键盘