RecyclerViewLoadMoreDemo【封装上拉加载功能的RecyclerView,搭配SwipeRefreshLayout实现下拉刷新】
版权声明:本文为HaiyuKing原创文章,转载请注明出处!
前言
封装含有上拉加载功能的RecyclerView,然后搭配SwipeRefreshLayout实现下拉刷新、上拉加载功能。
在项目中将原有的RecyclerView替换成WRecyclerView即可,不改动原有的adapter!
本Demo中演示了下拉刷新和分页功能,所以在将RecyclerView替换成WRecyclerView之后还需要添加其他代码(比如下拉刷新控件、无数据布局区域、分页相关代码),具体见Demo。
效果图
代码分析
WRecyclerView:自定义RecyclerView子类,在不改动 RecyclerView 原有 adapter 的情况下,使其拥有加载更多功能和自定义底部视图;
WRecyclerViewAdapter:自定义RecyclerView适配器;
WRecyclerViewFooter:自定义RecyclerView的底部上拉加载区域。
使用步骤
一、项目组织结构图
注意事项:
1、 导入类文件后需要change包名以及重新import R文件路径
2、 Values目录下的文件(strings.xml、dimens.xml、colors.xml等),如果项目中存在,则复制里面的内容,不要整个覆盖
二、导入步骤
(1)在build.gradle中引用recyclerview【版本号和appcompat保持一致】
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.why.project.recyclerviewloadmoredemo"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
//RecyclerView
compile "com.android.support:recyclerview-v7:27.1.1"
}
(2)在项目中实现Recyclerview基本数据展现
1、创建Bean类
package com.why.project.recyclerviewloadmoredemo.bean; /** * Created by HaiyuKing * Used 列表项的bean类 */ public class NewsBean { private String newsId;//id值 private String newsTitle;//标题 public String getNewsId() { return newsId; } public void setNewsId(String newsId) { this.newsId = newsId; } public String getNewsTitle() { return newsTitle; } public void setNewsTitle(String newsTitle) { this.newsTitle = newsTitle; } }
2、创建Adapter以及item的布局文件【后续不需要修改】
package com.why.project.recyclerviewloadmoredemo.adapter; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; import com.why.project.recyclerviewloadmoredemo.R; import com.why.project.recyclerviewloadmoredemo.bean.NewsBean; import java.util.ArrayList; /** * Created by HaiyuKing * Used 列表适配器 */ public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{ /**上下文*/ private Context myContext; /**集合*/ private ArrayList<NewsBean> listitemList; /** * 构造函数 */ public NewsAdapter(Context context, ArrayList<NewsBean> itemlist) { myContext = context; listitemList = itemlist; } /** * 获取总的条目数 */ @Override public int getItemCount() { return listitemList.size(); } /** * 创建ViewHolder */ @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(myContext).inflate(R.layout.news_list_item, parent, false); ItemViewHolder itemViewHolder = new ItemViewHolder(view); return itemViewHolder; } /** * 声明grid列表项ViewHolder*/ static class ItemViewHolder extends RecyclerView.ViewHolder { public ItemViewHolder(View view) { super(view); listItemLayout = (LinearLayout) view.findViewById(R.id.listitem_layout); mChannelName = (TextView) view.findViewById(R.id.tv_channelName); } LinearLayout listItemLayout; TextView mChannelName; } /** * 将数据绑定至ViewHolder */ @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int index) { //判断属于列表项 if(viewHolder instanceof ItemViewHolder){ NewsBean newsBean = listitemList.get(index); final ItemViewHolder itemViewHold = ((ItemViewHolder)viewHolder); itemViewHold.mChannelName.setText(newsBean.getNewsTitle()); //如果设置了回调,则设置点击事件 if (mOnItemClickLitener != null) { itemViewHold.listItemLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { int position = itemViewHold.getLayoutPosition();//在增加数据或者减少数据时候,position和index就不一样了 mOnItemClickLitener.onItemClick(itemViewHold.listItemLayout, position); } }); //长按事件 itemViewHold.listItemLayout.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { int position = itemViewHold.getLayoutPosition();//在增加数据或者减少数据时候,position和index就不一样了 mOnItemClickLitener.onItemLongClick(itemViewHold.listItemLayout, position); return false; } }); } } } /** * 添加Item--用于动画的展现*/ public void addItem(int position,NewsBean listitemBean) { listitemList.add(position,listitemBean); notifyItemInserted(position); } /** * 删除Item--用于动画的展现*/ public void removeItem(int position) { listitemList.remove(position); notifyItemRemoved(position); } /*=====================添加OnItemClickListener回调================================*/ public interface OnItemClickLitener { void onItemClick(View view, int position); void onItemLongClick(View view, int position); } private OnItemClickLitener mOnItemClickLitener; public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener) { this.mOnItemClickLitener = mOnItemClickLitener; } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/listitem_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_margin="1dp" android:background="#ffffff"> <TextView android:id="@+id/tv_channelName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="标题" android:textSize="18sp" android:padding="20dp"/> </LinearLayout>
3、在Activity布局文件中引用Recyclerview控件【引入WRecyclerView之后,需要修改】
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#F4F4F4"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="#00000000" android:divider="@null" android:listSelector="#00000000" android:scrollbars="none" /> </RelativeLayout>
4、在Activity类中初始化recyclerview数据【此时只是最基本的写法,后续还需要修改】
package com.why.project.recyclerviewloadmoredemo; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import com.why.project.recyclerviewloadmoredemo.adapter.NewsAdapter; import com.why.project.recyclerviewloadmoredemo.bean.NewsBean; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private ArrayList<NewsBean> mNewsBeanArrayList; private NewsAdapter mNewsAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); initDatas(); initEvents(); } private void initViews() { mRecyclerView = findViewById(R.id.recycler_view); } private void initDatas() { //初始化集合 mNewsBeanArrayList = new ArrayList<NewsBean>(); for(int i=0; i<6;i++){ NewsBean newsBean = new NewsBean(); newsBean.setNewsId("123"+i); newsBean.setNewsTitle("标题"+i); mNewsBeanArrayList.add(newsBean); } //设置布局管理器 LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(linearLayoutManager); //设置适配器 if(mNewsAdapter == null){ //设置适配器 mNewsAdapter = new NewsAdapter(this, mNewsBeanArrayList); mRecyclerView.setAdapter(mNewsAdapter); //添加分割线 //设置添加删除动画 //调用ListView的setSelected(!ListView.isSelected())方法,这样就能及时刷新布局 mRecyclerView.setSelected(true); }else{ mNewsAdapter.notifyDataSetChanged(); } } private void initEvents() { //列表适配器的点击监听事件 mNewsAdapter.setOnItemClickLitener(new NewsAdapter.OnItemClickLitener() { @Override public void onItemClick(View view, int position) { } @Override public void onItemLongClick(View view, int position) { } }); } }
(3)将WRecyclerView导入到项目中
1、将recyclerview包复制到项目中
package com.why.project.recyclerviewloadmoredemo.recyclerview; import android.content.Context; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; /** * Used 自定义RecyclerView,在不改动 RecyclerView 原有 adapter 的情况下,使其拥有加载更多功能和自定义底部视图。 * 参考资料:https://github.com/nukc/LoadMoreWrapper * @touch执行顺序:dispatchTouchEvent(Action_down)——onScrollStateChanged==RecyclerView.SCROLL_STATE_DRAGGING[1]——【onTouchEvent(Action_move)——onScrolled】循环——onTouchEvent(Action_up)——onScrollStateChanged==RecyclerView.SCROLL_STATE_IDLE[0] */ public class WRecyclerView extends RecyclerView { private static final String TAG = WRecyclerView.class.getSimpleName(); /**自定义适配器,用于在基础列表数据的基础上添加底部区域*/ private WRecyclerViewAdapter wRecyclerViewAdapter; /**自定义上拉加载的监听器*/ private WRecyclerViewListener mRecyclerListener; /**===================底部--上拉加载区域========================*/ private WRecyclerViewFooter mFooterView; /**是否启用上拉加载功能的标记:true-启用;false-禁用*/ private boolean mEnablePullLoad = true; /**上拉加载区域是否正在显示的标记*/ private boolean isShowFooter = false; /**是否处于加载状态的标记:true-加载;false-正常*/ private boolean mPullLoading = false; /**当前是否处于快速滑动的状态 :默认为false*/ private boolean isQuickSlide = false; /**当前是否处于下拉刷新的状态 :默认为false*/ private boolean isPullRefresh = false; /**快速移动产生惯性的移动距离值(自定义的临界值)*/ private final static int FAST_MOVE_DY = 150;//原先是100 /**recyclerView最后一个可见的item的下标值*/ private int lastVisibleItem = 0; /**recyclerView总item数目*/ private int mTotalItemCount = 0; public WRecyclerView(Context context) { super(context); initWithContext(context); } public WRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); initWithContext(context); } public WRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initWithContext(context); } private void initWithContext(Context context) { mFooterView = new WRecyclerViewFooter(context); /*解决bug: * 使用 RecyclerView 加官方下拉刷新的时候,如果绑定的 List 对象在更新数据之前进行了 clear,而这时用户紧接着迅速上滑 RV,就会造成崩溃,而且异常不会报到你的代码上,属于RV内部错误。 * 初次猜测是,当你 clear 了 list 之后,这时迅速上滑,而新数据还没到来,导致 RV 要更新加载下面的 Item 时候,找不到数据源了,造成 crash. * https://blog.csdn.net/lvwenbo0107/article/details/52290536*/ this.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (isPullRefresh) {//如果正在刷新,则不能滑动 return true; } else { return false; } } }); /**RecyclerView的滚动监听器*/ this.addOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); Log.w(TAG,"{onScrollStateChanged}newState="+newState); switch (newState){ case RecyclerView.SCROLL_STATE_DRAGGING:/*当前的recycleView被拖动滑动,类似action_down的效果1*/ isQuickSlide = false;//每一次开始滑动之前,设置快速滑动状态值为false if(mEnablePullLoad){ mFooterView.show();//因为快速滑动产生惯性的时候会进行隐藏底部上拉加载区域,所以需要在下次正常滑动之前在这里重新显示FootView } break; case RecyclerView.SCROLL_STATE_SETTLING:/*当前的recycleView在滚动到某个位置的动画过程,但没有被触摸滚动.调用 scrollToPosition(int) 应该会触发这个状态2*/ break; case RecyclerView.SCROLL_STATE_IDLE:/*停止滑动状态0*/ //isQuickSlide = false;//解开这个注释,就是不管快速滑动还是正常滑动,只要显示最后一条列表,就会自动加载数据 if(isShowFooter && !mPullLoading && !isQuickSlide){//也就是手指慢慢滑动出来foot区域的情况 startLoadMore(); //停止滑动后,如果上拉加载区域正在显示并且没有处于正在加载状态并且不是快速滑动状态,那么就开始加载 }else{ mFooterView.setState(WRecyclerViewFooter.STATE_NORMAL);//底部区域显示【查看更多】 } break; } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); //获取当前显示的最后一个子项下标值 LayoutManager layoutManager = recyclerView.getLayoutManager(); mTotalItemCount = layoutManager.getItemCount(); if (layoutManager instanceof GridLayoutManager) { lastVisibleItem = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions); int max = lastPositions[0]; for (int value : lastPositions) { if (value > max) { max = value; } } lastVisibleItem = max; } else { lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); } /* * 对于正常移动的情况,如果到了最后一个子项(底部上拉加载区域),并且启用了上拉加载功能 * */ if(mEnablePullLoad){ if(lastVisibleItem >= mTotalItemCount - 1) {//【实现松开手指后才会加载,暂时有问题--貌似没有问题了】 //if(lastVisibleItem >= mTotalItemCount - 1 - 1 ) {//减去的1,代表的footView,再减去的1代表下标值(0开始)【实现滑动到底部自动加载】 if(dy > 0){//向下滑动 isShowFooter = true; /* * 如果移动的距离大于FAST_MOVE_DY,则表明当前处于快速滑动产生惯性的情况,则隐藏底部上拉加载区域,这样就控制了底部上拉加载区域的不正常显示*/ if(dy > FAST_MOVE_DY){//当快速移动,产生惯性的时候,隐藏底部上拉加载区域 mFooterView.hide(); isQuickSlide = true;//只要有一次滑动的距离超过快速滑动临界值,则代表当前处于快速滑动状态 }else{ Log.w(TAG, "{onScrolled}mFooterView.getBottomMargin()="+mFooterView.getBottomMargin()); mFooterView.setState(WRecyclerViewFooter.STATE_READY);//底部区域显示【上拉加载更多】 } }else{ //向上滑动,不做任何处理 } }else { /*如果还没有到最后一个子项,前提条件是启用了上拉加载功能 * 则:(1)设置上拉加载区域显示状态值为false * (2)隐藏上拉加载区域*/ isShowFooter = false; mFooterView.hide(); } } } }); } /** * 设置适配器 * @param adapter */ public void setAdapter(Adapter adapter) { wRecyclerViewAdapter = new WRecyclerViewAdapter(adapter, mFooterView, mEnablePullLoad); super.setAdapter(wRecyclerViewAdapter); } /**是否正在上拉加载*/ public boolean ismPullLoading() { return mPullLoading; } /*==================================上拉加载功能==========================================*/ /** * 设置启用或者禁用上拉加载功能 * @param enable */ public void setPullLoadEnable(boolean enable) { mEnablePullLoad = enable; if (!mEnablePullLoad) { //禁用上拉加载功能 mFooterView.hide(); mFooterView.setOnClickListener(null); } else { //启用上拉加载功能 mPullLoading = false; mFooterView.show(); mFooterView.setState(WRecyclerViewFooter.STATE_NORMAL); // both "pull up" and "click" will invoke load more. mFooterView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startLoadMore(); } }); } /**解决当第一页就显示出来footview的时候,下拉刷新崩溃的问题 * java.lang.IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true * 是指view没有被recycled(回收),也就是foot区域没有被回收 * https://blog.csdn.net/u013106366/article/details/54024113*/ if(wRecyclerViewAdapter != null){ wRecyclerViewAdapter.setmEnablePullLoad(mEnablePullLoad); Log.w(TAG,"wRecyclerViewAdapter.notifyDataSetChanged()"); wRecyclerViewAdapter.notifyDataSetChanged(); } } /**开始加载,显示加载状态*/ private void startLoadMore() { if(mEnablePullLoad){ if(mPullLoading)return; mPullLoading = true; mFooterView.setState(WRecyclerViewFooter.STATE_LOADING); if (mRecyclerListener != null) { mRecyclerListener.onLoadMore(); } } } /** * 停止加载,还原到正常状态 */ public void stopLoadMore() { if(mEnablePullLoad){//启用上拉加载功能 if (mPullLoading == true) {//如果处于加载状态 mPullLoading = false; mFooterView.setState(WRecyclerViewFooter.STATE_NORMAL); mFooterView.hide(); } } } /**是否是否处于下拉刷新的状态*/ public void setPullRefresh(boolean pullRefresh) { isPullRefresh = pullRefresh; } /*==================================自定义下拉刷新和上拉加载的监听器==========================================*/ /** * 自定义下拉刷新和上拉加载的监听器 */ public interface WRecyclerViewListener { /**上拉加载*/ public void onLoadMore(); } public void setWRecyclerListener(WRecyclerViewListener l) { mRecyclerListener = l; } }
package com.why.project.recyclerviewloadmoredemo.recyclerview; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.AdapterDataObserver; import android.support.v7.widget.RecyclerView.ViewHolder; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.View; import android.view.ViewGroup; /** * Used 自定义RecyclerView适配器 */ public class WRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder>{ private static final String TAG = WRecyclerViewAdapter.class.getSimpleName(); private RecyclerView.Adapter adapter; /**是否启用上拉加载功能的标记:true-使用;false-禁用*/ private boolean mEnablePullLoad; /**上拉加载区域(foot区域)*/ private WRecyclerViewFooter mFooterView; //下面的ItemViewType是保留值(ReservedItemViewType),如果用户的adapter与它们重复将会强制抛出异常。不过为了简化,我们检测到重复时对用户的提示是ItemViewType必须小于10000 private static final int TYPE_FOOTER = 100001;//设置一个很大的数字,尽可能避免和用户的adapter冲突 private static final int TYPE_ITEM = 100002; public WRecyclerViewAdapter(RecyclerView.Adapter adapter, WRecyclerViewFooter mFooterView, boolean mEnablePullLoad) { this.adapter = adapter; this.mFooterView = mFooterView; this.mEnablePullLoad = mEnablePullLoad; } /** * 获取总的条目数 */ @Override public int getItemCount() { int itemCount = adapter.getItemCount(); if(mEnablePullLoad) {//如果启用上拉加载区域,那么就需要在原来的列表总数基础上加1 itemCount = itemCount + 1; } return itemCount; } @Override public int getItemViewType(int position) { if (isFooter(position)) { return TYPE_FOOTER; }else{ // return TYPE_ITEM;//不能return TYPE_ITEM,因为adapter中可能会设置不同的类型 return adapter.getItemViewType(position); } } /** * 判断是否属于上拉加载区域-即最后一行 */ public boolean isFooter(int position) { if(mEnablePullLoad) {//如果启用上拉加载区域,那么最后一行,就是总数目- 1 return position == getItemCount() - 1; }else { return false; } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == TYPE_FOOTER) { return new SimpleViewHolder(mFooterView); } return adapter.onCreateViewHolder(parent, viewType); } /**简单的ViewHolder*/ private class SimpleViewHolder extends ViewHolder { public SimpleViewHolder(View itemView) { super(itemView); } } /** * 将数据绑定至ViewHolder */ @Override public void onBindViewHolder(final ViewHolder holder, int position) { int adapterCount; if (adapter != null) { adapterCount = adapter.getItemCount(); if (position < adapterCount) { adapter.onBindViewHolder(holder, position); return; } } } @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 isFooter(position) ? gridManager.getSpanCount() : 1; } }); } adapter.onAttachedToRecyclerView(recyclerView); } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { adapter.onDetachedFromRecyclerView(recyclerView); } @Override public void onViewAttachedToWindow(ViewHolder holder) { super.onViewAttachedToWindow(holder); ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams && isFooter(holder.getLayoutPosition())) { StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; p.setFullSpan(true); } adapter.onViewAttachedToWindow(holder); } @Override public void onViewDetachedFromWindow(ViewHolder holder) { adapter.onViewDetachedFromWindow(holder); } @Override public void onViewRecycled(ViewHolder holder) { adapter.onViewRecycled(holder); } @Override public boolean onFailedToRecycleView(ViewHolder holder) { return adapter.onFailedToRecycleView(holder); } @Override public void unregisterAdapterDataObserver(AdapterDataObserver observer) { adapter.unregisterAdapterDataObserver(observer); } @Override public void registerAdapterDataObserver(AdapterDataObserver observer) { adapter.registerAdapterDataObserver(observer); } public boolean ismEnablePullLoad() { return mEnablePullLoad; } /**解决当第一页显示出来footview的时候刷新崩溃的问题*/ public void setmEnablePullLoad(boolean mEnablePullLoad) { this.mEnablePullLoad = mEnablePullLoad; } }
package com.why.project.recyclerviewloadmoredemo.recyclerview; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import com.why.project.recyclerviewloadmoredemo.R; /** * Used 自定义RecyclerView的底部上拉加载区域 */ public class WRecyclerViewFooter extends LinearLayout { private Context mContext; /**正常状态*/ public final static int STATE_NORMAL = 0; /**准备状态*/ public final static int STATE_READY = 1; /**加载状态*/ public final static int STATE_LOADING = 2; /**根节点*/ private View mContentView; /**含有进度条的布局区域*/ private View mProgressBarLayout; /**提示文字View*/ private TextView mHintView; public WRecyclerViewFooter(Context context) { super(context); initView(context); } public WRecyclerViewFooter(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } /**初始化*/ private void initView(Context context) { mContext = context; //添加底部上拉加载区域布局文件 LinearLayout moreView = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.wrecyclerview_footer, null); addView(moreView); moreView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); //实例化组件 mContentView = moreView.findViewById(R.id.wrecyclerview_footer_content); mProgressBarLayout = moreView.findViewById(R.id.wrecyclerview_footer_progressbar_layout); mHintView = (TextView)moreView.findViewById(R.id.wrecyclerview_footer_hint_textview); } /**更改加载状态: * @param state - STATE_NORMAL(0),STATE_READY(1),STATE_LOADING(2)*/ public void setState(int state) { //首先,将提示文字和进度条区域初始化隐藏 mHintView.setVisibility(View.INVISIBLE); mProgressBarLayout.setVisibility(View.INVISIBLE); //然后,根据状态值进行显示,隐藏这两个区域 if (state == STATE_READY) { //准备状态 mHintView.setVisibility(View.VISIBLE); Drawable drawable = ContextCompat.getDrawable(mContext,R.drawable.wrecyclerview_icon_pull); //setCompoundDrawables 画的drawable的宽高是按drawable.setBound()设置的宽高 //而setCompoundDrawablesWithIntrinsicBounds是画的drawable的宽高是按drawable固定的宽高,即通过getIntrinsicWidth()与getIntrinsicHeight()自动获得 drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight()); mHintView.setCompoundDrawables(null, drawable, null, null); mHintView.setText(R.string.wrecyclerview_footer_hint_ready); } else if (state == STATE_LOADING) { //加载状态 mProgressBarLayout.setVisibility(View.VISIBLE); } else { //正常状态 mHintView.setVisibility(View.VISIBLE); mHintView.setCompoundDrawables(null, null, null, null); mHintView.setText(R.string.wrecyclerview_footer_hint_normal); } } /** * 当禁用上拉加载功能的时候隐藏底部区域 */ public void hide() { LayoutParams lp = (LayoutParams)mContentView.getLayoutParams(); lp.height = 0;//这里设为0,那么虽然是显示,但是看不到 mContentView.setLayoutParams(lp); } /** * 显示底部上拉加载区域 */ public void show() { LayoutParams lp = (LayoutParams)mContentView.getLayoutParams(); lp.height = LayoutParams.WRAP_CONTENT; mContentView.setLayoutParams(lp); } /**设置布局的底外边距【暂时没有用到】*/ public void setBottomMargin(int height) { if (height < 0) return ; LayoutParams lp = (LayoutParams)mContentView.getLayoutParams(); lp.bottomMargin = height; mContentView.setLayoutParams(lp); } /**获取布局的底外边距【暂时没有用到】*/ public int getBottomMargin() { LayoutParams lp = (LayoutParams)mContentView.getLayoutParams(); return lp.bottomMargin; } }
2、将wrecyclerview_footer_progressbar.xml复制到项目中
<?xml version="1.0" encoding="utf-8"?> <!-- WRecyclerView上拉加载的进度条动画 --> <animated-rotate xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/wrecyclerview_footer_loading_rotate" android:pivotX="50%" android:pivotY="50%" />
3、将图片资源复制到项目中
4、将wrecyclerview_footer.xml复制到项目中
<?xml version="1.0" encoding="utf-8"?> <!-- WRecyclerView底部上拉加载的布局文件 --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/wrecyclerview_footer_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@color/wrecyclerview_footer_bg_color" android:gravity="center" > <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/wrecyclerview_footer_margin" android:paddingBottom="@dimen/wrecyclerview_footer_margin"> <!-- 正在加载的布局,默认隐藏 --> <LinearLayout android:id="@+id/wrecyclerview_footer_progressbar_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:visibility="invisible" > <!-- 自定义圆形进度条 --> <!-- android:indeterminateDrawable自定义动画图标 --> <ProgressBar android:layout_width="@dimen/wrecyclerview_footer_progressbar_WH" android:layout_height="@dimen/wrecyclerview_footer_progressbar_WH" android:indeterminateDrawable="@drawable/wrecyclerview_footer_progressbar" /> <!-- 正在加载 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/wrecyclerview_footer_margin" android:text="@string/wrecyclerview_footer_hint_loading" android:textColor="@color/wrecyclerview_footer_loading_text_color" android:textSize="@dimen/wrecyclerview_footer_text_size" /> </LinearLayout> <!-- 上拉加载更多的布局 (用来进行提示文字展现)--> <TextView android:id="@+id/wrecyclerview_footer_hint_textview" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="@string/wrecyclerview_footer_hint_normal" android:textColor="@color/wrecyclerview_footer_hint_text_color" android:textSize="@dimen/wrecyclerview_footer_text_size" android:drawableTop="@drawable/wrecyclerview_icon_pull" /> </RelativeLayout> </LinearLayout>
5、在colors.xml文件中添加以下代码
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorAccent">#FF4081</color> <!-- *********************SwipeRefreshLayout进度条颜色********************* --> <color name="swiperefresh_color_1">#5DB5F4</color> <color name="swiperefresh_color_2">#4E93D7</color> <color name="swiperefresh_color_3">#3689EE</color> <color name="swiperefresh_color_4">#5588FF</color> <!-- ************自定义WRecyclerview************ --> <color name="wrecyclerview_footer_bg_color">#EDEFF1</color> <color name="wrecyclerview_footer_hint_text_color">#70747E</color> <color name="wrecyclerview_footer_loading_text_color">#ABB1BD</color> <color name="wrecyclerview_divider_color">#eeeeee</color> </resources>
6、在dimens.xml文件中添加以下代码
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- ************自定义WRecyclerview************ --> <dimen name="wrecyclerview_footer_margin">10dp</dimen> <dimen name="wrecyclerview_footer_progressbar_WH">24dp</dimen> <dimen name="wrecyclerview_footer_text_size">17sp</dimen> </resources>
7、在strings.xml文件中添加以下代码
<resources> <string name="app_name">RecyclerViewLoadMoreDemo</string> <!-- ************自定义WRecyclerview************ --> <string name="wrecyclerview_footer_hint_normal">查看更多</string> <string name="wrecyclerview_footer_hint_ready">上拉加载更多</string> <string name="wrecyclerview_footer_hint_loading">正在拼命加载中…</string> </resources>
(4)将测试数据添加到项目中【实际项目中使用网络请求获取数据】
{
"data": [
{
"newsId": "0001",
"newsTitle": "标题1"
},
{
"newsId": "0002",
"newsTitle": "标题2"
},
{
"newsId": "0003",
"newsTitle": "标题3"
},
{
"newsId": "0004",
"newsTitle": "标题4"
},
{
"newsId": "0005",
"newsTitle": "标题5"
},
{
"newsId": "0006",
"newsTitle": "标题6"
},
{
"newsId": "0007",
"newsTitle": "标题7"
},
{
"newsId": "0008",
"newsTitle": "标题8"
},
{
"newsId": "0009",
"newsTitle": "标题9"
},
{
"newsId": "0010",
"newsTitle": "标题10"
}
],
"flag": "success",
"msg": "操作成功",
"total": 18
}
(5)将无数据区域布局文件添加到项目中【实际项目中根据需求更换布局以及提示图标】
<?xml version="1.0" encoding="utf-8"?> <!-- 无数据的占位图布局文件 --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/nodata_layout" android:orientation="vertical" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center"> <!-- 图片显示 --> <ImageView android:id="@+id/img_nodata" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" android:adjustViewBounds="true" android:contentDescription="@string/app_name" android:scaleType="centerCrop" android:layout_gravity="center" /> <!-- 提示语 --> <TextView android:id="@+id/tv_nodata" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暂无数据" android:textSize="16sp" android:textColor="#BADCFB" android:layout_gravity="center" android:layout_marginTop="8dp"/> </LinearLayout>
三、使用方法
在布局文件中使用下面的写法【根据实际情况修改WRecyclerView的路径】
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#F4F4F4"> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/list_swiperefreshlayout" android:layout_width="match_parent" android:layout_height="match_parent"> <com.why.project.recyclerviewloadmoredemo.recyclerview.WRecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="#00000000" android:divider="@null" android:listSelector="#00000000" android:scrollbars="none" /> </android.support.v4.widget.SwipeRefreshLayout> <!-- 无数据区域 --> <include layout="@layout/public_skeleton_screen_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> </RelativeLayout>
在Activity中的写法如下
package com.why.project.recyclerviewloadmoredemo; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.util.Log; import android.view.View; import android.widget.LinearLayout; import android.widget.Toast; import com.why.project.recyclerviewloadmoredemo.adapter.NewsAdapter; import com.why.project.recyclerviewloadmoredemo.bean.NewsBean; import com.why.project.recyclerviewloadmoredemo.recyclerview.WRecyclerView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private Context mContext; /**下拉刷新组件*/ private SwipeRefreshLayout swipe_container; private WRecyclerView mRecyclerView; private ArrayList<NewsBean> listitemList; private NewsAdapter mNewsAdapter; private int curPageIndex = 1;//当前页数 private int totalPage = 1;//总页数 private int pageSize = 10;//每一页的列表项数目 private int position = 1;//序号 private LinearLayout nodata_layout;//暂无数据区域 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mContext = this; initViews(); initSwipeRefreshView();//初始化SwipeRefresh刷新控件 initDatas(); } private void initViews() { mRecyclerView = findViewById(R.id.recycler_view); nodata_layout = findViewById(R.id.nodata_layout); } /** * 初始化SwipeRefresh刷新控件 */ private void initSwipeRefreshView() { swipe_container = (SwipeRefreshLayout) findViewById(R.id.list_swiperefreshlayout); //设置进度条的颜色主题,最多能设置四种 swipe_container.setColorSchemeResources(R.color.swiperefresh_color_1, R.color.swiperefresh_color_2, R.color.swiperefresh_color_3, R.color.swiperefresh_color_4); //调整进度条距离屏幕顶部的距离 scale:true则下拉的时候从小变大 swipe_container.setProgressViewOffset(true, 0, dip2px(mContext,10)); } /** * dp转px * 16dp - 48px * 17dp - 51px*/ private int dip2px(Context context, float dpValue) { float scale = context.getResources().getDisplayMetrics().density; return (int)((dpValue * scale) + 0.5f); } private void initDatas() { //===================网络请求获取列表===================== //解决在第二页的时候进行查询的时候返回到该界面时显示的还是第二页数据 position = 1; curPageIndex = 1; mNewsAdapter = null;//将列表适配器置空,否则查询界面点击确定按钮的时候无法更新列表数据 //初始化集合 listitemList = new ArrayList<NewsBean>(); //设置布局管理器 LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(linearLayoutManager); initListData(); } //初始化第一页数据 private void initListData() { //模拟网络请求返回数据 getBackDataList(curPageIndex,pageSize); } private void getBackDataList(int curPageIndex, int pageSize) { //模拟网络请求获取数据(一般curPageIndex、pageSize都需要传过去,这里是模拟,所以没有用到pageSize) String responseStr = getStringFromAssert(MainActivity.this,"pagenum"+curPageIndex + ".txt"); //模拟网络请求的回调方法 onBefore(); onResponse(responseStr); new Handler().postDelayed(new Runnable() { @Override public void run() { onAfter();//模拟网络请求一定的延迟后执行 } },2000); } private void onBefore() { if(!swipe_container.isRefreshing()) {//实现当下拉刷新的时候,不需要显示加载对话框 Toast.makeText(mContext,"显示加载框",Toast.LENGTH_SHORT).show(); //showProgressDialog();//显示进度加载框 } } private void onResponse(String response) { try { if (response != null && !"".equals(response) && !"{}".equals(response)){ if(response.startsWith("jsonp(")){ response = response.substring(6,response.length() - 1); } JSONObject responseObj = new JSONObject(response); if(responseObj.getString("flag").equals("success")){ JSONArray listArray = responseObj.getJSONArray("data"); if(listArray.length() > 0){//如果有筛选功能,则需要使用大于等于,如果没有,只是单纯的刷新,则使用大于即可【不过因为fail的情况下显示无数据区域,所以此处去掉==0的情况】 //计算总页数 int totalPage = responseObj.getInt("total") % pageSize == 0 ? responseObj.getInt("total") / pageSize : responseObj.getInt("total") / pageSize + 1;//计算总页数 ArrayList<NewsBean> listitemList_temp = new ArrayList<NewsBean>(); for(int i=0;i<listArray.length();i++){ JSONObject listItemObj = listArray.getJSONObject(i); NewsBean newsBean = new NewsBean(); newsBean.setNewsId(listItemObj.getString("newsId")); newsBean.setNewsTitle(listItemObj.getString("newsTitle")); listitemList_temp.add(newsBean); } showList(totalPage,listitemList_temp); }else { showListFail("数据内容为空"); } }else { showListFail("数据内容为空"); } } else { showListFail("数据内容为空"); } }catch (JSONException e) { Toast.makeText(mContext,"服务器数据解析异常,请联系管理员!",Toast.LENGTH_SHORT).show(); }catch (Exception e) { Toast.makeText(mContext,"服务器数据解析异常,请联系管理员!",Toast.LENGTH_SHORT).show(); } } //显示列表 public void showList(int totalPage, List<NewsBean> mNewsBeanList){ this.totalPage = totalPage; if(curPageIndex == 1){ listitemList.clear();//下拉刷新,需要清空集合,因为刷新的是第一页数据【解决下拉刷新后立刻上拉加载崩溃的bug,方案二】 } if(mNewsBeanList != null && mNewsBeanList.size() > 0) { switchNoDataVisible(false);//显示列表,隐藏暂无数据区域 for (NewsBean newsBean : mNewsBeanList) { listitemList.add(newsBean); } } } private void onAfter() { Toast.makeText(mContext,"隐藏加载框",Toast.LENGTH_SHORT).show(); //dismissProgressDialog();//隐藏进度加载框 if(curPageIndex == 1){//如果首页数据为空或者小于每页展现的条数,则禁用上拉加载功能 if(listitemList.size() < pageSize){ mRecyclerView.setPullLoadEnable(false);//禁用上拉加载功能 }else{ mRecyclerView.setPullLoadEnable(true);//启用上拉加载功能 } } //设置适配器 if(mNewsAdapter == null){ //设置适配器 mNewsAdapter = new NewsAdapter(this, listitemList); mRecyclerView.setAdapter(mNewsAdapter); //添加分割线 //设置添加删除动画 //调用ListView的setSelected(!ListView.isSelected())方法,这样就能及时刷新布局 mRecyclerView.setSelected(true); }else{ mNewsAdapter.notifyDataSetChanged(); } stopRefreshAndLoading();//停止刷新和上拉加载 //初始化监听事件 initEvents(); } //获取列表数据失败 public void showListFail(String msg) { //当第一页数据为空的时候,才会执行 if(curPageIndex == 1){ //扩展:显示无数据占位图 switchNoDataVisible(true); }else{ Toast.makeText(mContext,msg,Toast.LENGTH_SHORT).show(); } } /**切换无数据和列表展现/隐藏*/ private void switchNoDataVisible(boolean showNoData){ if(showNoData){ nodata_layout.setVisibility(View.VISIBLE); swipe_container.setVisibility(View.GONE); }else if(nodata_layout.getVisibility() == View.VISIBLE){ nodata_layout.setVisibility(View.GONE); swipe_container.setVisibility(View.VISIBLE); } } private void initEvents() { //为SwipeRefreshLayout布局添加一个Listener,下拉刷新 swipe_container.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { refreshList();//刷新列表 } }); //自定义上拉加载的监听 mRecyclerView.setWRecyclerListener(new WRecyclerView.WRecyclerViewListener() { @Override public void onLoadMore() { Log.w(TAG, "onLoadMore-正在加载"); curPageIndex = curPageIndex + 1; if (curPageIndex <= totalPage) { initListData();//更新列表项集合 } else { //到达最后一页了 Toast.makeText(mContext,"我也是有底线滴",Toast.LENGTH_SHORT).show(); //隐藏正在加载的区域 stopRefreshAndLoading(); } } }); //列表适配器的点击监听事件 mNewsAdapter.setOnItemClickLitener(new NewsAdapter.OnItemClickLitener() { @Override public void onItemClick(View view, int position) { } @Override public void onItemLongClick(View view, int position) { } }); } /**刷新列表*/ private void refreshList() { mRecyclerView.setPullLoadEnable(false);//禁用上拉加载功能 mRecyclerView.setPullRefresh(true);//设置处于下拉刷新状态中 curPageIndex = 1; position = 1; //listitemList.clear();//下拉刷新,需要清空集合,因为刷新的是第一页数据【解决下拉刷新后立刻上拉加载崩溃的bug,方案二】 initListData();//更新列表项集合 } /** * 停止刷新和上拉加载 */ private void stopRefreshAndLoading() { //检查是否处于刷新状态 if(swipe_container.isRefreshing()){ //显示或隐藏刷新进度条,一般是在请求数据的时候设置为true,在数据被加载到View中后,设置为false。 swipe_container.setRefreshing(false); } //如果正在加载,则获取数据后停止加载动画 if(mRecyclerView.ismPullLoading()){ mRecyclerView.stopLoadMore();//停止加载动画 } mRecyclerView.setPullRefresh(false);//设置处于下拉刷新状态中[否] } /*===========读取assets目录下的js字符串文件(js数组和js对象)===========*/ /** * 访问assets目录下的资源文件,获取文件中的字符串 * @param filePath - 文件的相对路径,例如:"listdata.txt"或者"/www/listdata.txt" * @return 内容字符串 * */ public String getStringFromAssert(Context mContext, String filePath) { String content = ""; // 结果字符串 try { InputStream is = mContext.getResources().getAssets().open(filePath);// 打开文件 int ch = 0; ByteArrayOutputStream out = new ByteArrayOutputStream(); // 实现了一个输出流 while ((ch = is.read()) != -1) { out.write(ch); // 将指定的字节写入此 byte 数组输出流 } byte[] buff = out.toByteArray();// 以 byte 数组的形式返回此输出流的当前内容 out.close(); // 关闭流 is.close(); // 关闭流 content = new String(buff, "UTF-8"); // 设置字符串编码 } catch (Exception e) { Toast.makeText(mContext, "对不起,没有找到指定文件!", Toast.LENGTH_SHORT) .show(); } return content; } }
混淆配置
无
参考资料
Error: Inconsistency detected. Invalid item position 11(offset:11).state:37 RecyclerView
Scrapped or attached views may not be recycled
手把手教你实现RecyclerView的下拉刷新和上拉加载更多
你必须了解的RecyclerView的五大开源项目-解决上拉加载、下拉刷新和添加Header、Footer等问题