Android列表详解

Posted by 阿呆 on 2019-01-23

转载声明:在文章开头处以以下形式标明转载信息

作者:余丹

链接:http://blogyudan.online

前言

RecyclerView 是我们用的最多的控件,也是一个较为复杂的控件,本文的目的在于疏通RecyclerView的基本用法以及原理,不对源码以及内部组件进行过于深入的讨论(当然,有兴趣的话也会说一说),结合一些自己在开发中的经验,谈谈如何设计一个流畅的列表。

带着问题来思考

1.RecyclerView.Adapter的几个函数的作用(getItemCount,)

2.Recycler的复用机制

3.如何编写多Item,如何添加footer与header

4.RecyclerView滑动卡顿的优化,RecyclerView资源是如何释放的,哪些情况容易出现OOM,有没有可能出现内存泄漏

5.数据的刷新机制,如何做到最优化刷新

6.LayoutManager

7.如何实现瀑布流

8.复杂动画的实现,以及滑动冲突的解决

ViewHolder

首先,我们需要了解一下什么是ViewHolder;ViewHolder最初是出现在ListView的BaseAdapter中,作为一种优化手段,我们来看一下普通的ListView的getView方法就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = null;
v = li.inflate(R.layout.list_text, parent, false);

String text = (String)getItem(position);

TextView tv = (TextView) convertView.findViewById(R.id.tv);
tv.setText(text);

Log.i("-getView-", String.valueOf(counter));
return v;
}

getView的作用有以下两点:

1.引入布局来构造一个新的ItemView

2.为该ItemView绑定数据(前提是实例化ItemView的内部控件)

可能你就会问,convertView为什么没有用?它是什么?对于ListView而言,它的内部维护了一个ItemView的缓存队列,划出屏幕的View不会立即被回收,而是会进入一个缓存队列,当这个队列满的时候,最早进入的View才会被回收。这里的convertView实际上就是缓存队列中的View。

我们注意到,getView这样一个过程实际上可以被优化,首先,我们不用每次都inflate新的布局出来,而是判断有没有缓存,也就是判断convertView==null?,如果converView!=null,则可以直接view = convertView。另一个可以优化的点,则是控件的实例化,findViewById也是需要不小的开销的,我们可不可以在第一次实例化控件的时候,就把它绑定到ItemView上面,这样,下次接收到convertView的时候,就可以直接获取到相关的实例。以上就是关于ListView的一些优化

反映到代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null){
converView = li.inflate(R.layout.list_text, parent, false);
}

String text = (String)getItem(position);

TextView tv = (TextView) convertView.findViewById(R.id.tv);
ViewHolder vh = new ViewHolder(tv);
tv.setText(text);

convertView.setTag(vh);

return convertView;
}

class ViewHolder{
TextView tv;
public ViewHolder(TextView tv){
this.tv = tv;
}
}

RecyclerView.Adapter

RecyclerView.Adapter里面有很多的函数,我选取几个常用的来说明一下它们各自的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract static class Adapter<VH extends RecyclerView.ViewHolder> {
private final RecyclerView.AdapterDataObservable mObservable =
new RecyclerView.AdapterDataObservable();
private boolean mHasStableIds = false;

public Adapter() {
}

@NonNull
public abstract VH onCreateViewHolder(@NonNull ViewGroup var1, int var2);

public void onBindViewHolder(@NonNull VH holder, int position, @NonNull
List<Object> payloads) {
}

@NonNull
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
.....
}

public int getItemViewType(int position) {
return 0;
}

public abstract int getItemCount();

下面的分析过程,是按照实际的业务流程来走的,也便于你了解到 RecyclerView 到底是如何工作的

1.getItemCount

首先,对于一个RecyclerView来说,它在不断向下滑动的过程中,需要知道什么时候滑动到了底部,这个可以通过 getItemCount来得到,当超过了ItemCount的时候,列表不再滑动

2.getItemType(int position)

现在我们知道何何时划到底部了,那中间的过程呢?假设我们现在划到了第n项,通过这个函数,我们知道即将划出来的那个Item的Type,当Item即将划入屏幕(包括初始化界面哈)的时候,要开始一系列的操作,上面得到了该Item的Type,那么这里就要开始根据这个Type来加载对应的布局了,首先,RecyclerView会判断是否有缓存的holder,由于多ItemType的情况,RecyclerView内部会根据ItemType维护几个不同的缓存队列(我的猜想,等我有空看源码验证一下),如果有,则直接bindViewHolder,没有的话先调用 onCreateViewHolder来引入新的布局和ViewHolder,最后将创建的ViewHolder返回

3.onCreateViewHolder( ViewGroup parent , int viewType )

注意,返回的不仅是控件的实例哈,我们写的ViewHolder必须继承自 RecyclerView.ViewHolder,并在构造方法中调用super,这样,RecyclerView.ViewHolder内部有一个ItemView会将布局实例保存下来,所以,你不需要再单独地将ViewHolder和ItemView绑定,因为RecyclerView已经帮你绑定好了。

4.onBindViewHolder( ViewHolder vh, int position, List list)

这里就是将数据绑定到vh里面啦,很简单,就不作过多的说明了,唯一一点需要注意的是,有时候会出现Item内容错乱的情况,很大概率是绑定数据的时候,没有清空原来的数据,当然,这种一般是发生在条件绑定的情况,就是某些条件下才绑定某些数据,因为如果每次都绑定所有控件的话,相当于也是把原来的数据清空了。

一个简单的多ItemType的Adapter的实现

多ItemType的情况有很多种,处理方式也不太一样,但是关键的点,无非就在于,在 getItemType 中获取到 type,然后分别在 onCreateViewHolder 和 onBindViewHolder 中进行分支操作。这里以 一个添加了 header 和 footer 的 Rv 的Adapter 伪码作为实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// List<Course> courses;
public MAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
public final int TYPE_HEADER = 1;
public final int TYPE_FOOTER = 2;
public final int TYPE_COURSE = 3;

// 也可能是list里面的数据有不同的 type,可以根据具体的情况来判断 type
public int getItemType(int position){
int count = getItemCount();
if(position == 0)
{
return TYPE_HEADER;
}else if(position < count)
{
return TYPE_COURSE;
}else
{
return TYPE_FOOTER;
}
}

public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int
viewType)
{
switch(viewType){
case TYPE_HEADER:
return new HeadHolder(layoutInflater.inflate(.....));
break;
case TYPE_COURSE:
return new CourseHolder(...);
break;
case TYPE_FOOTER:
return new FooterHolder(....)
break;
}
}

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch(holder.getItemViewType()){
case TYPE_HEADER:
bindHeader(holder,position);
break;
case ..
....
}
}

public void bindHeader(RecyclerView.ViewHolder vh,int position){
....
}

public void bindCourse()
public void bindFooter()
....
}

多ItemType关于数据源的处理

这算是比较简单的情况了,有时候我们会碰到有两个数据源的情况,这时我们该如何处理呢?

1
2
List<A> listA;  // 对应布局 item_a
List<B> listB; // 对应布局 item_b

这种情况涉及到以下几个问题

1.listA和listB都是通过构造函数传进Adapter么?

2.界面的刷新是等数据全部加载完,还是每加载完一个刷新一下?

3.刷新的方式?如何才是比较优雅的刷新?

采用原始泛型的思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
----- MainActivity -----------
List<Object> data;
data.addAll(listA);
data.addAll(listB);
adapter = new Adapter(data)

----- Adapter ---------------
public int getItemType(int position){
Object object = data.get(position);
if(object.instanceOf(A)){
return TYPE_A;
}else{
return TYPE_B;
}
}

// 在绑定的适合进行强转
public void onBindViewHolder(....., List<Object> data){
if(holder.getType==TYPE_A){
bindA(holder,(A)data.get(position));
}else{
bindB(holder,(B)data.get(position));
}
}

实现一个Model接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface Model{
// 空实现
}

-------- 数据源 ---------------
public class A implements Model{
....
public static final int TYPE_A = 1;
}
B同上

-------- MainActivity ----------
List<Model> data;
List<A> listA;
List<B> listB;

data.addAll(listA);
data.addAll(listB);
adapter = new Adapter(.... , data)

-------- Adapter ----------------
public int getItemType(int position){
Model model = data.get(position);
if(model.instanceOf(A)){
return TYPE_A;
}else{
return TYPE_B;
}
}

关于 Model接口,一开始我写了个 getType 方法在里面,这样就不用在判断type的时候根据类名来判断了,不过后来非猿哥告诉我,这样不太好,这不该是Model该做的工作,而是Adapter的任务,所以,我就把这个任务改成了空实现。

官方的 Rv就是这样,现在有很多不错的开源的封装库,能让我们更好地使用 RecyclerView,这里强烈推荐以下库:

一个更好用更强大的RecyclerView框架 :Flap

我会专门开一篇文章来介绍 Flap 的使用以及实现原理,以及我们为什么称它为当前最强大易用的 RecyclerView 使用框架。而且我继承了FlapAdapter增加添加Header和footer的功能。

关于Header与Footer

关于刷新与加载

现在有很多的图片上拉刷新,下拉加载框架,所以,个人觉得把这个交给这些框架来做就好,不要用header或者footer去做,很麻烦 = =,当然,大佬请无视我的话哈

Header与Footer解决什么

有很多的页面现在都是由一个 Banner 和下面的列表项组成,而且,通常的设计是 Banner 可以和 列表项一起滑动,

Header需要刷新数据(不难,在List里面加上数据就好,在onBind的时候绑定)

如何添加Header与Footer

主要两种方式:

1.将header与footer加入数据源 data

2.不加入data,直接在Adapter里面做一些适配(不太适合需要刷新数据的Header与Footer)

LayoutManager