最近项目中用到下拉刷新和上拉加载,于是在网上找了许多轮子,发现很多都有瑕疵,大多都没有解决当前item不满一页时,上拉加载的View一直显示的问题。之后自己尝试解决该问题,并完善了其他功能,就写了如下demo,看下面:
代码地址:github
一、下拉刷新:
使用的是SwipeRefrshLayout,在support-v4兼容包下,作为官方的下拉刷新控件,其功能还很强大的,也不详细介绍了,网上的使用详解一大堆,这里讲一下基本使用方法:
- setOnRefreshListener(OnRefreshListener):添加下拉刷新监听器
- setRefreshing(boolean):显示或者隐藏刷新进度条
- isRefreshing():检查是否处于刷新状态
- setColorSchemeResources():设置进度条的颜色主题,最多设置四种。
然后在xml中:
Java代码中添加刷新事件:
mSrl=(SwipeRefreshLayout)findViewById(R.id.sr_load_more);
mSrl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// 调用刷新方法
}
});
效果:
是不是很简单呢?具体使用可以去看下官方文档,下面我们来说上拉加载。
二、上拉加载:
首先,确定需求:
- 当RecyclerView加载时,只显示RecyclerView的item;
- 用户上拉,拉到最后一项item时,出现下拉刷新的footerView,并进行数据加载;
- 数据加载完成后,footerView消失,并将数据添加进RecyclerView;
- 用户再次下拉,重复上面的步骤。
这里有几点需要注意:
- RecyclerView初次加载时,当item不满一页时,footerView应隐藏;
- 数据加载失败(网络错误等)时,footView应提示,并取消上拉加载事件,添加点击重新加载事件;
- 服务器数据全部加载完成后,footView也应提示,并取消上拉加载事件。
代码实现:
首先,我们定义RecyclerView的item和footerView:
item的xml布局:
footerView的xml布局:
嗯,看起来比较复杂,其实就是三个状态的布局,我们可以在代码中控制其显示、隐藏。
然后,我们来写RecyclerView的适配器。
首先,我们知道RecyclerView适配器中,有这个方法:
public int getItemViewType(int position) {
return 0;
}
利用这个方法,我们可以判断item是普通item还是底部的FooterView,我们定义两个类型:
/**
* 普通项和底部
*/
private final int TYPE_ITEM=1;
private final int TYPE_FOOTER=2;
然后,重写getItemViewType方法和getItemCount方法:
@Override
public int getItemViewType(int position) {
if(position+1==getItemCount()){ // 到了底部
return TYPE_FOOTER;
}else {
return TYPE_ITEM;
}
}
@Override
public int getItemCount() {
return mData.size()!=0?mData.size()+1:0;
}
然后,创建我们的ViewHolder,并且重写onCreateViewHolder方法,在这里,我们根据itemType,来决定要加载的布局:
private View mFootView;
class MyViewHolder extends RecyclerView.ViewHolder{
public TextView tvTest;
public LinearLayout llLoading; // 正在加载
public LinearLayout llLoadError; // 错误
public LinearLayout llLoadedAll; // 全部加载完
public MyViewHolder(View itemView) {
super(itemView);
// 判断是否是底部加载的view
if(mFootView==null || itemView!=mFootView){
tvTest=(TextView)itemView.findViewById(R.id.tv_item_test);
}
if(mFootView!=null && mFootView==itemView){
llLoading=(LinearLayout)itemView.findViewById(R.id.ll_footer_loading);
llLoadError=(LinearLayout)itemView.findViewById(R.id.ll_footer_error);
llLoadedAll=(LinearLayout)itemView.findViewById(R.id.ll_footer_all_loaded);
}
}
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType==TYPE_ITEM){
// 普通item
View view= LayoutInflater.from(mContext).inflate(R.layout.item_rv_test,parent,false);
return new MyViewHolder(view);
} else{
// 底部
mFootView=LayoutInflater.from(mContext).inflate(R.layout.item_first_footer,parent,false);
return new MyViewHolder(mFootView);
}
}
接下来是最重要的onBindViewHolder方法,在这里,我们首先根据itemType,判断ViewHolder数据,然后,判断footerView的状态:
/**
* 底部上拉加载的view有四种状态:
* 初始时:隐藏(避免recyclerView的item不满一页时,上拉加载的view一直显示)
* 第一次滑动后:显示(显示信息为正在加载)
* 网络错误时:显示(显示信息为网络错误,点击重新加载)
* 全部都已加载完时:显示(显示信息为已全部加载完毕)
*/
public static final int STATE_FIRST=1;
public static final int STATE_SHOW=2;
public static final int STATE_ERROR_NET=3;
public static final int STATE_ALL_LOADED=4;
/**
* 当前加载状态
*/
private int mLoadState=STATE_FIRST;
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
if(getItemViewType(position)==TYPE_ITEM){ // 普通项
holder.tvTest.setText(mData.get(position));
}else { // 底部
switch (mLoadState){ // 判断底部上拉加载的view的状态
case STATE_FIRST: // 初始状态 view为隐藏
mFootView.setVisibility(View.GONE);
holder.llLoading.setVisibility(View.VISIBLE);
holder.llLoadError.setVisibility(View.GONE);
holder.llLoadedAll.setVisibility(View.GONE);
break;
case STATE_SHOW:
mFootView.setVisibility(View.VISIBLE);
holder.llLoading.setVisibility(View.VISIBLE);
holder.llLoadError.setVisibility(View.GONE);
holder.llLoadedAll.setVisibility(View.GONE);
break;
case STATE_ERROR_NET:
mFootView.setVisibility(View.VISIBLE);
holder.llLoading.setVisibility(View.GONE);
holder.llLoadError.setVisibility(View.VISIBLE);
holder.llLoadedAll.setVisibility(View.GONE);
// 重新加载
if(mOnListener!=null){
holder.llLoadError.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mOnListener.onReLoad();
}
});
}
break;
case STATE_ALL_LOADED:
mFootView.setVisibility(View.VISIBLE);
holder.llLoading.setVisibility(View.GONE);
holder.llLoadError.setVisibility(View.GONE);
holder.llLoadedAll.setVisibility(View.VISIBLE);
break;
}
}
}
private OnListener mOnListener;
// item点击事件和底部重新加载点击事件
public interface OnListener{
void onItemClick();
void onReLoad();
}
恩,主要代码就是这样,然后我们就可以在Activity中设置RecyclerView的点击事件、RecyclerView的滑动事件:
// recyclerView的item点击事件,和网络错误时重新加载的点击事件
mAdapter.setListener(new RvLoadMoreAdapter.OnListener() {
@Override
public void onItemClick() { // item点击事件
}
@Override
public void onReLoad() { // 重新加载
}
});
// 监听RecyclerView的滑动事件,判断是否是手指向上滑动,避免item不满一页时,下拉刷新触发上拉加载事件。
private boolean isUp=false;
mRv.setOnTouchListener(new View.OnTouchListener() {
float oldy;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
oldy=event.getY();
break;
case MotionEvent.ACTION_UP:
if(oldy-event.getY()>ViewConfiguration.get(mContext).getScaledTouchSlop()){
isUp=true;
} else {
isUp=false;
}
}
return false; // 不拦截事件
}
});
mRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
private int lastVisibleItemPosition;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//正在滚动
if(isUp && !mAdapter.ismIsLoading() && newState==RecyclerView.SCROLL_STATE_IDLE &&
lastVisibleItemPosition+1==mAdapter.getItemCount()){ // 判断条件:手指上滑、当前不是正在加载的状态、停止滑动、末尾的item为最后一项
// 初始状态时,footer为隐藏,这里将其设为可见
mAdapter.showFooter();
if(mAdapter.getLoadState()==RvLoadMoreAdapter.STATE_FIRST){ //初始状态
mAdapter.setmLoadState(false,false,null);
}
// 只有显示状态时,才能响应滑动事件(不包括错误状态和加载完成状态)
if(mAdapter.getLoadState()==RvLoadMoreAdapter.STATE_SHOW){ // 显示状态
mAdapter.setmIsLoading(true);
// 这里写加载事件
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItemPosition=mLayoutManager.findLastVisibleItemPosition();
}
});
基本逻辑就是这样了,具体代码可以看demo,演示如下:
代码地址:github