自定义 RecyclerView 实现刷新与加载更多
“哎呀,最近新接手一个项目里要用 RecyclerView 的地方好多,而且基本上都需要上拉刷新和下拉加载,我才写了两个页面,手都快断了,有没有什么比较好的方法?”
“什么!你还在自己一点点地写?那你得写到啥时候去?”
“那怎么办?”
“你是不是傻,Github 上那么多优秀的开源库,用一个啊。或者是自己写一个简单的,别写那么复杂的,能满足自己需求的就行。”
“有道理,我去看看”
......
“Github 上的优秀的好多啊,我都看花了眼,不知道该选哪一个?”
“实在不行,你就自己写一个呗,如果只是实现你的需求应该不难,来说说看,你的需求是什么?”
“也不是很复杂,就简单的一个,能上拉加载更多,下拉刷新的就行,简单不?”
“你出去别说你是程序员,会被怼死的……”
“咋了,这不行吗?”
“不是不行,你这需求提了跟没提一个样,有没有点实质性的东西?”
“嗯……,那我重说
- 在布局文件中只使用一个控件,不做任何的嵌套;
- 在 RecyclerView 没有数据的时候,页面上显示提示信息,‘暂无数据’ 就行;
- 支持下拉刷新,刷新显示的 Header 就用官方的那个小圆圈就行了;
- 支持上拉加载,如果有下一页数据, Footer 显示一个 Loading 配上‘加载中……’,如果没有数据,就显示‘你已经扯到底了’。
嗯,就这么多。”
“还行,也不算难,有什么想法吗?”
“暂时没了,后面想到再加吧。”
“…………”
“那行,那我们先来看看具体怎么实现吧。”
“1. 同一个控件内既能刷新又要可以上拉加载,官方的控件应该是没有的,那就自定义一个吧,自定义一个 ViweGroup ,然后把 SwipeRefreshLayout 和 RecyclerView 包裹起来,然后再给 RecyclerView 添加一个滑动监听事件;
- 在没有数据的情况下显示提示信息,这个可以放一个 TextView 或者是 ImageView ,然后在获取数据之后,判断是否为空,如果数据不为空就显示 RecyclerView,如果数据为空,就显示 TextView;
- 需要动态的修改 Footer 显示的内容,那就在 RecyclerView 的 Adapter 中动态更改呗。
”
“等等,那个 Footer 的动态更改能不能不让我做啊,每次都要写动态改变代码,实在不想写。”
“没让你写啊,我是说放在我们内部的 Adapter 里面。是这样的,我打算自己实现一个 Adapter ,在这个 Adapter 里面去动态的更新 Footer。”
“你实现了 Adapter,那我外面还能用吗?
“可以啊,咋不行,我自定义的 Adapter 把你外层需要用到的 Adapter 包裹起来,你还是你,不过,你外面还有一层。玩过俄罗斯套娃吗?”
“没有……”
“想想手机和手机壳的关系,这个跟那个类似。”
“好像懂了……”
“那我们来实现吧。”
“从哪下手啊?我怎么感觉一脸懵逼。”
“先从布局文件开始,先来写显示出来的列表布局,在里面写上你要的那几个控件。然后再给 Footer 来一个布局。”
me_recyclerview.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.AppCompatTextView
android:id="@+id/list_tip_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="没有数据"
android:textSize="16sp" />
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/list_swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/list_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
“嗯,这就是列表布局了,内容很简单就是你现在写的样子……,好了,再来写个 Footer 布局。”
“等等,这个简单,我来。”
“行行行,你来。”
me_foot.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="48dp">
<ProgressBar
android:id="@+id/item_footer_progress"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.4"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/item_footer_message"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:gravity="center"
android:text="加载中"
app:layout_constraintLeft_toRightOf="@+id/item_footer_progress" />
</android.support.constraint.ConstraintLayout>
“还行,那我们继续?”
“好嘞。”
“刚才说了,需要使用一个 Adapter 来包裹外层调用方的 Adapter ,那么还需要自定义一个 Adapter ,就叫 MeRefreshListAdapter 吧。来继续”
MeRefreshListAdapter.java
private static final int TYPE_FOOTER = -1;
private RecyclerView.Adapter adapter;
private LayoutInflater inflater;
private boolean isShowFooter;
private boolean isNoData;
public MeRefreshListAdapter(RecyclerView.Adapter adapter, Context context, boolean isShowFooter, boolean isNoData) {
this.adapter = adapter;
this.isShowFooter = isShowFooter;
this.isNoData = isNoData;
inflater = LayoutInflater.from(context);
}
“你这里那个 RecyclerView.Adapter 干啥使得?”
“那个就是外面你传进来的 Adapter 啊,你外面的 Adapter 肯定得继承自 RecyclerView.Adapter 吧,那我得保证,你加的什么泛型我这边都能用啊。比如你有一个是继承自 RecyclerView.Adapter<Person.ViewHolder>,还有一个是继承自 RecyclerView.Adapter<City.ViewHolder>”。
“哦~”
“还有,这个 MeRefreshListAdapter 需要控制 Footer 和外层 Adapter 的 Item 的创建与绑定,总的来说,他们是两类,我不管你外层的 Item 的类型有几个,我都要加上 1 ,这个 1 就是 Footer。并且,我内部只对 Footer 这一个类型的 Item 进行控制,其余的还是交给你外层去做。不过这时候就需要进行判断了,到底哪一个是 Footer 类型的,哪一个是普通类型(外层的 Item 类型)。”
“这个我知道,简单,Footer 肯定是在最后一个的,那就直接把最后一个归为 Footer 就行了,不对,当不显示 Footer 的时候,最后一个 Item 也是普通类型,那就是在显示 Footer 的情况下的最后一个 Item 是 Footer。”
“嗯,是的,这样一来,我们自定义的 Adapter 也就出来了。”
MeRefreshListAdapter.java
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder;
switch (viewType) {
case TYPE_FOOTER:
View footer = inflater.inflate(R.layout.me_list_footer, parent, false);
viewHolder = new FooterHolder(footer);
break;
default:
viewHolder = adapter.onCreateViewHolder(parent, viewType);
break;
}
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof FooterHolder) {
FooterHolder footerHolder = (FooterHolder) holder;
footerHolder.progressBar.setVisibility(isNoData ? View.GONE : View.VISIBLE);
footerHolder.message.setText(isNoData ? "--- 扯到底了 ---" : "加载中……");
}else{
adapter.onBindViewHolder(holder,position);
}
}
@Override
public int getItemCount() {
return isShowFooter ? adapter.getItemCount() + 1 : adapter.getItemCount();
}
@Override
public int getItemViewType(int position) {
if (isShowFooter && position + 1 == getItemCount()) {
return TYPE_FOOTER;
} else {
return adapter.getItemViewType(position);
}
}
static class FooterHolder extends RecyclerView.ViewHolder {
ProgressBar progressBar;
TextView message;
FooterHolder(View itemView) {
super(itemView);
progressBar = itemView.findViewById(R.id.item_footer_progress);
message = itemView.findViewById(R.id.item_footer_message);
}
}
public void setShowFooter(boolean flag) {
this.isShowFooter = flag;
this.notifyDataSetChanged();
}
public void setNoData(boolean flag) {
this.isNoData = flag;
this.notifyDataSetChanged();
}
“当然了,还需要对外开放更新 Adapter 的方法,这样才能实时控制嘛。”
“我们接下来干啥?”
“写接口,我们需要自定义两个接口方法,用来在外层调用。很简单,就下面这样的。”
RefreshLoadListener.java
public interface RefreshLoadListener {
/**
* 上拉加载更多
*/
void upLoad();
/**
* 下拉刷新
*/
void downRefresh();
}
“接口也写完了,现在开始进入正题了。自定义 ViewGroup 继承自 LinearLayout,然后重写其中的构造方法。四个构造方法都要重写啊,不写的话可能会运行报错。”
MeRecyclerView.java
public class MeRecyclerView extends LinearLayout implements SwipeRefreshLayout.OnRefreshListener {
private SwipeRefreshLayout refreshLayout;
private RecyclerView refreshList;
private AppCompatTextView listTip;
private RefreshLoadListener loadListener;
private MeRefreshListAdapter meRefreshListAdapter;
private Context mContext;
private RecyclerView.LayoutManager layoutManager;
private boolean isShowFooter;
private boolean isNoData;
private int lastVisibleItem;
public MeRecyclerView(Context context) {
super(context);
init(context);
}
public MeRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MeRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public MeRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
this.mContext = context;
LayoutInflater.from(context).inflate(R.layout.me_recyclerview, this, true);
refreshLayout = findViewById(R.id.list_swipe_refresh);
listTip = findViewById(R.id.list_tip_message);
refreshList = findViewById(R.id.list_list);
refreshLayout.setOnRefreshListener(this);
}
}
“你看,刚才在初始化操作的时候,我们就设置了 SwipeRefreshLayout 的刷新接口,现在就到了我们需要实现他的时候了,可能跟你平时写的不太一样,因为虽然我们实现了这个接口,但我们还是得把这个具体实现的机会交给外层,让外层进行实际的数据获取。当然了,我们如果当前页面处于刷新状态,那 Footer 肯定是不能显示出来的,这个时候就需要操作一下刚才我们写的 MeRefreshListAdapter ”
MeRecyclerView.java
@Override
public void onRefresh() {
if (null != loadListener) {
isNoData = false;
isShowFooter = false;
meRefreshListAdapter.setNoData(isNoData);
meRefreshListAdapter.setShowFooter(isShowFooter);
loadListener.downRefresh();
}
}
“好了,下拉刷新结束了,是不是很简单?”
“这就结束了,这也太快了。”
“下面来看上拉加载更多。”
“刷新的容易,加载更多的,要控制下面 Footer 显示还要改变显示的文字,想想头就大。”
“你想啥呢,那些都做完了啊,刚在在 MeRefreshListAdapter 不是就做过了……”
“完全没意识到……”
“上拉加载其实跟刷新一样的,都挺好实现的,无非就是给 RecyclerView 添加一个滑动监听,然后再根据当前位置去判断是否加载新数据。你看”
MeRecyclerView.java
refreshList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
int totalCount = layoutManager.getItemCount() - 1;
if (totalCount > 18 && newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem == totalCount && !isNoData && !isShowFooter) {
if (null != loadListener) {
setFooter();
loadListener.upLoad();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (layoutManager instanceof LinearLayoutManager) {
lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
}
}
});
private void setFooter() {
isShowFooter = true;
meRefreshListAdapter.setShowFooter(true);
}
"好了,上拉加载更多也解决了,不过这里我偷了个懒。在获取当前列表的最后一项的 position 时,我判断了 LinearLayoutManager ,其他的没进行判断,不过也简单,GridLayoutManager 是继承自 LinearLayoutManager ,而 StaggeredGridLayoutManager 获取最后一个 item 的 position 返回的是一个数组,取出其中最大的即可。"
“好吧,那这样是不是就算完成了啊?”
“想多了,看起来,我们是做完这么多的,但是,并没有做完,比如,我们还需要进行停止刷新,要不然那个无论是上拉加载还是刷新都会有那个 loading 在那不停地动。”
MeRecyclerView.java
public void stopRefresh(int pageCount, boolean isNoData) {
this.isNoData = isNoData;
meRefreshListAdapter.setNoData(isNoData);
showData(meRefreshListAdapter.getItemCount() > 0);
if (pageCount == 1) {
refreshLayout.setRefreshing(false);
} else {
if (!isNoData) {
isShowFooter = false;
meRefreshListAdapter.setShowFooter(isShowFooter);
}
}
}
private void showData(boolean b) {
refreshList.setVisibility(b ? VISIBLE : VISIBLE);
listTip.setVisibility(b ? GONE : VISIBLE);
}
"再比如,我们还需要与外部的 LayoutManager 以及 Adapter 建立联系以及在外部数据变动的时候,通知我们进行刷新"
MeRecyclerView.java
public void setLayoutManager(RecyclerView.LayoutManager layoutManager) {
this.layoutManager = layoutManager;
refreshList.setLayoutManager(layoutManager);
}
public void setAdapter(RecyclerView.Adapter adapter) {
meRefreshListAdapter = new MeRefreshListAdapter(adapter, mContext, isShowFooter, isNoData);
refreshList.setAdapter(meRefreshListAdapter);
}
public void notifyDataSetChanged() {
meRefreshListAdapter.notifyDataSetChanged();
}
"这还不算完……"
“还没完?”
“肯定的啊,你想啊,页面刚打开的时候,你是不是得立即去刷新一下,做事总得主动点嘛,所以我们还需要一个可以开始刷新的。”
“我知道,是让页面进来的时候去刷新一下,开始获取数据对吧,我们可以直接调用下拉刷新的方法啊,那样最省事。就像这样。”
public void startRefresh() {
refreshLayout.setRefreshing(true);
onRefresh();
}
“是的,这么样就行了。不过还有一个最最最重要的就是得让外部把接口实现了,所以还要这么一个方法。”
public void setLoadListener(RefreshLoadListener loadListener) {
this.loadListener = loadListener;
}
"这个,我肯定不会晚啊,不过如果忘记了,肯定都不能用啊。我先拿去试试看好不好用啊。"
“……”
“真爽,用起来特简单。在布局文件里面只用一个控件,就是刚才我们自定义的那个,然后,像往常一样设置 LayoutManager 和 Adapter 不过需要在服务器返回数据之后自己手动停止刷新。还是挺简单的。哦,对,还有一个要自己启动刷新。”
RefreshActivity.java
@BindView(R.id.refresh_demo_list)
MeRecyclerView refreshDemoList;
int page = 1;
int size = 20;
List<Map<String, Object>> dataList;
ListAdapter listAdapter;
LinearLayoutManager layoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_refresh);
ButterKnife.bind(this);
dataList = new ArrayList<>();
listAdapter = new ListAdapter(this, R.layout.item_simgle_check, dataList);
layoutManager = new LinearLayoutManager(this);
refreshDemoList.setLoadListener(this);
refreshDemoList.setLayoutManager(layoutManager);
refreshDemoList.setAdapter(listAdapter);
}
@Override
protected void onResume() {
super.onResume();
refreshDemoList.startRefresh();
}
@Override
public void downRefresh() {
page = 1;
getData();
}
@Override
public void upLoad() {
page += 1;
getData();
}
private void update(List<Map<String, Object>> maps) {
if (page > 5) {
maps.clear();
}
if (maps.size() > 0) {
if (page == 1) {
dataList.clear();
dataList.addAll(maps);
} else {
dataList.addAll(maps);
}
refreshDemoList.notifyDataSetChanged();
refreshDemoList.stopRefresh(page, false);
} else {
refreshDemoList.stopRefresh(page, true);
}
}
![](https://img.haomeiwen.com/i2803883/8e541a3b2f3f777b.png)
![](https://img.haomeiwen.com/i2803883/b518fbd6a5fede39.png)
![](https://img.haomeiwen.com/i2803883/97f7660ada18d4c6.png)
"好了,跟你学完了,我还去继续写代码了,还有什么问题,下次再来问你。希望有一天,我能不问你也能把问题解决了。👍"