Android Tech And Thoughts.

ListView与RecyclerView用法

Word count: 6.8kReading time: 32 min
2019/11/30 Share

itemview.png

前言

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
@Override
2
public View getView(int position, View convertView, ViewGroup parent) {
3
   	View v = null;
4
    v = li.inflate(R.layout.list_text, parent, false);
5
 
6
    String text = (String)getItem(position);
7
 
8
    TextView tv = (TextView) convertView.findViewById(R.id.tv);
9
    tv.setText(text);
10
 
11
    Log.i("-getView-", String.valueOf(counter));
12
    return v;
13
}

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
@Override
2
public View getView(int position, View convertView, ViewGroup parent) {
3
    if(convertView == null){
4
        converView =  li.inflate(R.layout.list_text, parent, false);
5
    }
6
 
7
    String text = (String)getItem(position);
8
 
9
    TextView tv = (TextView) convertView.findViewById(R.id.tv);
10
    ViewHolder vh = new ViewHolder(tv);
11
    tv.setText(text);
12
    
13
    convertView.setTag(vh);
14
 
15
    return convertView;
16
}
17
18
class ViewHolder{
19
    TextView tv;
20
    public ViewHolder(TextView tv){
21
        this.tv = tv;
22
    }
23
}

关于多ItemType的处理:

多ItemType是如何实现的,事实上,ListView他不会关心你几个type,它只需要Adapter在恰当的位置提供一个恰当的ItemView给他就 OK 了,这一切都是Adapter来操心的。那么Adapter是怎么处理多ItemType的呢,关键的地方在于 convertView 这个参数,当你调用 getView 的时候,你可以通过 data.get(position) 来找到对应的数据,那么 View 呢,我们怎么知道传递进来的 convertView 是不是我需要的 ViewType, 这点设计者也想到了,事实上,Adapter 内部缓存了好几条View队列,数量等于 TypeCount,在调用 getView 之前,会预先调用 getItemViewType 这个函数,来得到你需要的视图类型,再将其传递给 getView 函数,如果 convertView 为 null,并不表示缓存队列为空,而仅仅代表该 Type 的 缓存队列为空。这时候我们inflate一个新的View就可以了。

RecyclerView.Adapter

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

1
public abstract static class Adapter<VH extends RecyclerView.ViewHolder> {
2
        private final RecyclerView.AdapterDataObservable mObservable =
3
            	new RecyclerView.AdapterDataObservable();
4
        private boolean mHasStableIds = false;
5
6
        public Adapter() {
7
        }
8
9
        @NonNull
10
        public abstract VH onCreateViewHolder(@NonNull ViewGroup var1, int var2);
11
12
        public void onBindViewHolder(@NonNull VH holder, int position, @NonNull 
13
                                     List<Object> payloads) {
14
        }
15
16
        @NonNull
17
        public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
18
            .....
19
        }
20
21
        public int getItemViewType(int position) {
22
            return 0;
23
        }
24
    
25
   		public abstract int getItemCount();

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

1.getItemCount

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

2.getItemType(int position)

实现机制和BaseAdapter一致,参考ListView的分析

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
// List<Course> courses;
2
public MAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
3
{
4
	public final int TYPE_HEADER = 1;
5
	public final int TYPE_FOOTER = 2;
6
	public final int TYPE_COURSE = 3;
7
	
8
	// 也可能是list里面的数据有不同的 type,可以根据具体的情况来判断 type
9
	public int getItemType(int position){
10
		int count = getItemCount();
11
		if(position == 0)
12
        {
13
        	return TYPE_HEADER;
14
        }else if(position < count)
15
        {
16
        	return TYPE_COURSE;
17
        }else
18
        {
19
			return TYPE_FOOTER;
20
		}
21
	}
22
	
23
	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int 
24
								viewType)
25
    {
26
    	switch(viewType){
27
    		case TYPE_HEADER:
28
    			return new HeadHolder(layoutInflater.inflate(.....));
29
    			break;
30
    		case TYPE_COURSE:
31
    			return new CourseHolder(...);
32
    			break;
33
    		case TYPE_FOOTER:
34
    			return new FooterHolder(....)
35
    			break;
36
    	}
37
    }
38
    
39
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
40
    	switch(holder.getItemViewType()){
41
    		case TYPE_HEADER:
42
    			bindHeader(holder,position);
43
    			break;
44
    		case ..
45
    		....
46
    	}
47
    } 
48
    
49
    public void bindHeader(RecyclerView.ViewHolder vh,int position){
50
    	....
51
    }
52
    
53
    public void bindCourse()
54
    public void bindFooter()
55
    ....
56
}

多ItemType关于数据源的处理

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

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

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

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

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

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

采用原始泛型的思想

1
----- MainActivity -----------
2
List<Object> data;
3
data.addAll(listA);
4
data.addAll(listB);
5
adapter = new Adapter(data)
6
7
----- Adapter ---------------
8
public int getItemType(int position){
9
	Object object = data.get(position);
10
    if(object.instanceOf(A)){
11
        return TYPE_A;
12
    }else{
13
        return TYPE_B;
14
    }
15
}
16
17
// 在绑定的适合进行强转
18
public void onBindViewHolder(....., List<Object> data){
19
    if(holder.getType==TYPE_A){
20
		bindA(holder,(A)data.get(position));
21
    }else{
22
        bindB(holder,(B)data.get(position));
23
    }
24
}

实现一个Model接口

1
public interface Model{
2
    // 空实现
3
}
4
5
--------  数据源 ---------------
6
public class A implements Model{
7
	....
8
    public static final int TYPE_A = 1;
9
}
10
B同上
11
12
-------- MainActivity ----------
13
List<Model> data;
14
List<A> listA;
15
List<B> listB;
16
17
data.addAll(listA);
18
data.addAll(listB);
19
adapter = new Adapter(.... , data)
20
21
-------- Adapter ----------------
22
public int getItemType(int position){
23
    Model model = data.get(position);
24
    if(model.instanceOf(A)){
25
        return TYPE_A;
26
    }else{
27
        return TYPE_B;
28
    }
29
}

关于 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)

ListView的几个需要注意的地方

OnIntemSelectedListener

这个Listener仅在按键模式下有用(待考证,讲的比较笼统),在触摸模式下,这个一般不会回调,onItemSelected指的就是选中了当前Item(TV模式下背景会变成已选中),但此时还不会执行 onClick。

网上有一些办法可以解决ListView初始化的时候自动调用 onItemClick 的问题。

多 ItemType 的Type定义的问题

之前在实现一个多 ItemType的ListView的时候,addScrpaView 方法遇到了数组溢出问题.后来查阅一些资料,发现问题出在Type的定义上

1
public static final int TYPE_1 = 1024;
2
public static final int TYPE_2 = 2048

在定义 Type 的时候,最好是从0定义,目前没有仔细阅读源码,不知道为什么会出现这样的问题,不过记住这个先

ListView源码分析

Adapter的作用

将数据适配工作与View结构解耦。Adapter是数据源,AdapterView是展示数据源的UI控件,Adapter是给AdapterView使用的,通过调用AdapterView的setAdapter方法就可以让一个AdapterView绑定Adapter对象,从而AdapterView会将Adapter中的数据展示出来。

adapter.png

Adapter的作用除了适配数据之外,还有一个非常重要的作用,就是 getView (包含了数据绑定的步骤),这也是为了解耦,这样ListView才能完全地与数据解耦(不必绑定数据)

需要关注一个方法

Adapter # getItemViewType:ListView 如果想要实现多 ItemType,则必须实现这个方法,不实现当然也可以,但是你就无法重用 ConvertView 了,因为当无法取到对应的 Type 的缓存队列的话,convertView 默认就会返回默认的Type,这样在绑定数据的时候就会出现错误

RecycleBin 机制

RecycleBin 是 AbsListView 的一个内部类,其主要作用就是提供了 ItemView 的缓存机制,即重用划出屏幕的 View。

AbsListView -> RecycleBin

1
/**
2
 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
3
 * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
4
 * those views which were onscreen at the start of a layout. By
5
 * construction, they are displaying current information. At the end of
6
 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
7
 * are old views that could potentially be used by the adapter to avoid
8
 * allocating views unnecessarily.
9
 *  RecycleBin维护两个Level的存储:ActiveViews和ScrapViews
10
 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
11
 * @see android.widget.AbsListView.RecyclerListener
12
 */
13
class RecycleBin {
14
	private RecyclerListener mRecyclerListener;
15
 
16
	/**
17
	 * The position of the first view stored in mActiveViews.
18
	 */
19
	private int mFirstActivePosition;
20
 
21
	/**
22
	 * Views that were on screen at the start of layout. This array is
23
	 * populated at the start of layout, and at the end of layout all view
24
	 * in mActiveViews are moved to mScrapViews. Views in mActiveViews
25
	 * represent a contiguous range of Views, with position of the first
26
	 * view store in mFirstActivePosition.
27
	 */
28
	private View[] mActiveViews = new View[0];
29
 
30
	/**
31
	 * Unsorted views that can be used by the adapter as a convert view.
32
	 */
33
	private ArrayList<View>[] mScrapViews;
34
 
35
	private int mViewTypeCount;
36
 
37
	private ArrayList<View> mCurrentScrap;
38
 
39
	/**
40
	 * Fill ActiveViews with all of the children of the AbsListView.
41
	 * @param childCount The minimum number of views mActiveViews should hold
42
	 * @param firstActiPosition The position of the first view that will be stored in mActiveViews
43
	 */
44
-----------
45
// 这个方法在 RecycleBin 里有点异类。它的作用是将 AbsListView的所有子View添加到 mActiveViews 数组里
46
// 它主要是为了防止第二次 layout,要重新 inflate 新的 view。这也是它闪光的唯一时刻
47
	void fillActiveViews(int childCount, int firstActivePosition) {
48
		if (mActiveViews.length < childCount) {
49
			mActiveViews = new View[childCount];
50
		}
51
		mFirstActivePosition = firstActivePosition;
52
		final View[] activeViews = mActiveViews;
53
		for (int i = 0; i < childCount; i++) {
54
			View child = getChildAt(i);
55
			AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
56
			// Don't put header or footer views into the scrap heap
57
			if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
58
				// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
59
				// active views.
60
				// However, we will NOT place them into scrap views.
61
				activeViews[i] = child;
62
			}
63
		}
64
	}
65
 
66
	/**
67
	 * Get the view corresponding to the specified position. The view will
68
	 * be removed from mActiveViews if it is found.
69
	 * @param position The position to look up in mActiveViews
70
	 * @return The view if it is found, null otherwise
71
	 */
72
	View getActiveView(int position) {
73
		int index = position - mFirstActivePosition;
74
		final View[] activeViews = mActiveViews;
75
		if (index >= 0 && index < activeViews.length) {
76
			final View match = activeViews[index];
77
			activeViews[index] = null;
78
			return match;
79
		}
80
		return null;
81
	}
82
                    
83
  //Move all views remaining in mActiveViews to mScrapViews.                 
84
    void scrapActiveViews() {
85
       //....
86
    }
87
 
88
	/**
89
	 * Put a view into the ScapViews list. These views are unordered.
90
	 * @param scrap The view to add
91
	 */
92
	void addScrapView(View scrap) {
93
		AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
94
		if (lp == null) {
95
			return;
96
		}
97
		// Don't put header or footer views or views that should be ignored
98
		// into the scrap heap
99
		int viewType = lp.viewType;
100
		if (!shouldRecycleViewType(viewType)) {
101
			if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
102
				removeDetachedView(scrap, false);
103
			}
104
			return;
105
		}
106
		if (mViewTypeCount == 1) {
107
			dispatchFinishTemporaryDetach(scrap);
108
			mCurrentScrap.add(scrap);
109
		} else {
110
			dispatchFinishTemporaryDetach(scrap);
111
			mScrapViews[viewType].add(scrap);
112
		}
113
 
114
		if (mRecyclerListener != null) {
115
			mRecyclerListener.onMovedToScrapHeap(scrap);
116
		}
117
	}
118
 
119
	/**
120
	 * @return A view from the ScrapViews collection. These are unordered.
121
	 */
122
	View getScrapView(int position) {
123
		ArrayList<View> scrapViews;
124
		if (mViewTypeCount == 1) {
125
			scrapViews = mCurrentScrap;
126
			int size = scrapViews.size();
127
			if (size > 0) {
128
				return scrapViews.remove(size - 1);
129
			} else {
130
				return null;
131
			}
132
		} else {
133
			int whichScrap = mAdapter.getItemViewType(position);
134
			if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
135
				scrapViews = mScrapViews[whichScrap];
136
				int size = scrapViews.size();
137
				if (size > 0) {
138
					return scrapViews.remove(size - 1);
139
				}
140
			}
141
		}
142
		return null;
143
	}
144
 
145
	public void setViewTypeCount(int viewTypeCount) {
146
		if (viewTypeCount < 1) {
147
			throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
148
		}
149
		// noinspection unchecked
150
		ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
151
		for (int i = 0; i < viewTypeCount; i++) {
152
			scrapViews[i] = new ArrayList<View>();
153
		}
154
		mViewTypeCount = viewTypeCount;
155
		mCurrentScrap = scrapViews[0];
156
		mScrapViews = scrapViews;
157
	}
158
 
159
}

这个方法在 RecycleBin 里有点异类。它的作用是将 AbsListView的所有子View添加到 mActiveViews 数组里
// 它主要是为了防止第二次 layout,要重新 inflate 新的 view。这也是它闪光的唯一时刻

mActiveViews[] vs mScrapVies[]

RecycleBin 中维护了两个很重要的数组

  • mActiveViews[] : 当前在页面(onScreen)的 View .这个数组可以说是在不断变化的,最初是空的,即便是在滑动过程中,也会在 n+1和n+2之间波动 。 特别要注意的是,如果一个 ItemView 滑动出了屏幕,它就会被 mActiveVies 移除出数组,并且新进入屏幕的 View,如果他不是从 mActivieViews 中直接重用的,它就会被加入 mActiveViews . 关于直接重用。关于直接重用这个概念有一点需要清楚,我们的ListView不是说只有在新的 Item 进来了之后才重绘,实际上,只要是视图改变了(滑动),它都要重绘,重绘之前,他的子View们都被Dettah了,之前的子View 仍然要重新布局,重新attach,这时候,不过我们的 数据还在,mActivitViews 还储存着之前的 onScreen 的Views,这时候就可以重用了。
  • mScrapViews[] : 当前被回收的 View,也就是 offScreen 的View,当然这个最初也是0,在华东过程中它的实际大小也会波动。

FillActiveViews

这个方法在 RecycleBin 里有点异类。它的作用是将 AbsListView的所有子View添加到 mActiveViews 数组里
。它主要是为了防止第二次 layout,要重新 inflate 新的 view。这也是它闪光的唯一时刻。

RecycleBin 的基本原理

当ListView加载一个子View的时候(onScreen),它会做如下的判断:

itemview.png

上面是 RecycleBin 的核心思想,RecycleBin 提供的相应的API 也是点到为止,比如数组内元素的添加和删除等工作,机制的实现还得靠 ListView 相关的方法(主要就是 ListView 的 onLayout 方法)。下面,我们深入一下源代码,看看 ListView 是如何实现这些的。

ListView # onLayout

写在前面:

  • View是一帧一帧绘制的,每一帧绘制都经历了measure->layout->draw这三个阶段,绘制完一帧之后,如果UI需要更新,比如用户滚动了ListView,那么又会绘制下一帧,再次经历measure->layout->draw方法。
  • ListView # childCount : childCount 并不是 Adapter 中数据数目,而是显示在 ListView 中的 Item 数目(onScreen)。其实也很好理解,虽然ListView里面可能缓存了很多的 View ,但是真正 attach 到 ListView 的数量其实是有限但不一定的(会在 n+1~n+2 的范围内波动)
  • ListView # getChildAt( position ):结合上面的概念,也就不难理解了,position也并非指代在数据源(List)中的位置,而是处于ListView中(onScreen)的位置,比如 getChildAt(0) 返回的就是 firstPosition(这个则是 ListView 记录的对应 Adapter中的位置) 的View。

第一次 Layout:

ListView并没有重写 onLayout ,那我们看看父类 AbsListView 中的 onLayout 方法

1
/**
2
 * Subclasses should NOT override this method but {@link #layoutChildren()}
3
 * instead.
4
 */
5
@Override
6
protected void onLayout(boolean changed, int l, int t, int r, int b) {
7
	super.onLayout(changed, l, t, r, b);
8
	mInLayout = true;
9
	if (changed) {
10
		int childCount = getChildCount();
11
		for (int i = 0; i < childCount; i++) {
12
			getChildAt(i).forceLayout();
13
		}
14
		mRecycler.markChildrenDirty();
15
	}
16
	layoutChildren();
17
	mInLayout = false;
18
}

可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码如下所示:

1
@Override
2
protected void layoutChildren() {
3
    final boolean blockLayoutRequests = mBlockLayoutRequests;
4
    if (!blockLayoutRequests) {
5
        mBlockLayoutRequests = true;
6
    } else {
7
        return;
8
    }
9
    try {
10
        super.layoutChildren();
11
        invalidate();
12
        if (mAdapter == null) {
13
            resetList();
14
            invokeOnItemScrollListener();
15
            return;
16
        }
17
        int childrenTop = mListPadding.top;
18
        int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
19
        int childCount = getChildCount();
20
        int index = 0;
21
        int delta = 0;
22
        View sel;
23
        View oldSel = null;
24
        View oldFirst = null;
25
        View newSel = null;
26
        View focusLayoutRestoreView = null;
27
        
28
        // Remember stuff we will need down below
29
        // 好像是为了记录之前的状态(比如选中的是哪个 Position),这段代码可以先关注
30
        switch (mLayoutMode) {
31
       		 case LAYOUT_SET_SELECTION:
32
            	index = mNextSelectedPosition - mFirstPosition;
33
            	if (index >= 0 && index < childCount) {
34
                	newSel = getChildAt(index);
35
            	}
36
            	break;
37
        	case LAYOUT_FORCE_TOP:
38
        	case LAYOUT_FORCE_BOTTOM:
39
        	case LAYOUT_SPECIFIC:
40
        	case LAYOUT_SYNC:
41
           	 break;
42
        	case LAYOUT_MOVE_SELECTION:
43
        	default:
44
            	// Remember the previously selected view
45
            	index = mSelectedPosition - mFirstPosition;
46
            	if (index >= 0 && index < childCount) {
47
              	  oldSel = getChildAt(index);
48
            	}
49
            	// Remember the previous first child
50
            	oldFirst = getChildAt(0);
51
            	if (mNextSelectedPosition >= 0) {
52
              	  delta = mNextSelectedPosition - mSelectedPosition;
53
            	}
54
            	// Caution: newSel might be null
55
           	 newSel = getChildAt(index + delta);
56
       	 }
57
       	 
58
        boolean dataChanged = mDataChanged;
59
        if (dataChanged) {
60
            handleDataChanged();
61
        }
62
        // Handle the empty set by removing all views that are visible
63
        // and calling it a day
64
        if (mItemCount == 0) {
65
            resetList();
66
            invokeOnItemScrollListener();
67
            return;
68
        } else if (mItemCount != mAdapter.getCount()) {
69
            throw new IllegalStateException("The content of the adapter has changed but "
70
                    + "ListView did not receive a notification. Make sure the content of "
71
                    + "your adapter is not modified from a background thread, but only "
72
                    + "from the UI thread. [in ListView(" + getId() + ", " + getClass() 
73
                    + ") with Adapter(" + mAdapter.getClass() + ")]");
74
        }
75
        setSelectedPositionInt(mNextSelectedPosition);
76
        // Pull all children into the RecycleBin.
77
        // These views will be reused if possible
78
        final int firstPosition = mFirstPosition;
79
        final RecycleBin recycleBin = mRecycler;
80
        // reset the focus restoration
81
        View focusLayoutRestoreDirectChild = null;
82
        // Don't put header or footer views into the Recycler. Those are
83
        // already cached in mHeaderViews;
84
        if (dataChanged) {
85
            for (int i = 0; i < childCount; i++) {
86
                recycleBin.addScrapView(getChildAt(i));
87
                if (ViewDebug.TRACE_RECYCLER) {
88
                    ViewDebug.trace(getChildAt(i),
89
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
90
                }
91
            }
92
        } else {
93
//-----------------------
94
//这句代码很关键,我们将缓存所有的 ActiveViews。由此可以看出好像并不是每添加一个View,就保存一个,而是批量保存
95
            recycleBin.fillActiveViews(childCount, firstPosition);
96
        }
97
        // take focus back to us temporarily to avoid the eventual
98
        // call to clear focus when removing the focused child below
99
        // from messing things up when ViewRoot assigns focus back
100
        // to someone else
101
        final View focusedChild = getFocusedChild();
102
        if (focusedChild != null) {
103
            // TODO: in some cases focusedChild.getParent() == null
104
            // we can remember the focused view to restore after relayout if the
105
            // data hasn't changed, or if the focused position is a header or footer
106
            if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
107
                focusLayoutRestoreDirectChild = focusedChild;
108
                // remember the specific view that had focus
109
                focusLayoutRestoreView = findFocus();
110
                if (focusLayoutRestoreView != null) {
111
                    // tell it we are going to mess with it
112
                    focusLayoutRestoreView.onStartTemporaryDetach();
113
                }
114
            }
115
            requestFocus();
116
        }
117
        // Clear out old views
118
        detachAllViewsFromParent();
119
        switch (mLayoutMode) {
120
        	case LAYOUT_SET_SELECTION:
121
            	if (newSel != null) {
122
                	sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
123
            	} else {
124
                	sel = fillFromMiddle(childrenTop, childrenBottom);
125
            	}
126
            	break;
127
        	case LAYOUT_SYNC:
128
            	sel = fillSpecific(mSyncPosition, mSpecificTop);
129
            	break;
130
        	case LAYOUT_FORCE_BOTTOM:
131
            	sel = fillUp(mItemCount - 1, childrenBottom);
132
            	adjustViewsUpOrDown();
133
            	break;
134
        	case LAYOUT_FORCE_TOP:
135
            	mFirstPosition = 0;
136
            	sel = fillFromTop(childrenTop);
137
            	adjustViewsUpOrDown();
138
            	break;
139
        	case LAYOUT_SPECIFIC:
140
            	sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
141
            	break;
142
        	case LAYOUT_MOVE_SELECTION:
143
            	sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
144
            	break;
145
//-------
146
            // 核心代码
147
       	 default:
148
        			// 第一次 onLayout 会走这里
149
            	if (childCount == 0) {
150
            			// 默认填充方式:从上往下
151
                	if (!mStackFromBottom) {
152
                    	final int position = lookForSelectablePosition(0, true);
153
                    	setSelectedPositionInt(position);
154
                    	// 从上开始往下填充
155
                    	sel = fillFromTop(childrenTop);
156
               		 } else {
157
                    	final int position = lookForSelectablePosition(mItemCount - 1, false);
158
                    	setSelectedPositionInt(position);
159
                    	sel = fillUp(mItemCount - 1, childrenBottom);
160
                	}
161
            	} else {
162
               	 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
163
                   	 sel = fillSpecific(mSelectedPosition,
164
                  	          oldSel == null ? childrenTop : oldSel.getTop());
165
                	} else if (mFirstPosition < mItemCount) {
166
                	    sel = fillSpecific(mFirstPosition,
167
                   	         oldFirst == null ? childrenTop : oldFirst.getTop());
168
                	} else {
169
                 	   sel = fillSpecific(0, childrenTop);
170
                	}
171
           	  }
172
           	  break;
173
       	 }
174
//---------------------------
175
        // Flush any cached views that did not get reused above
176
        recycleBin.scrapActiveViews();
177
        if (sel != null) {
178
            // the current selected item should get focus if items
179
            // are focusable
180
            if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
181
                final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
182
                        focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
183
                if (!focusWasTaken) {
184
                    // selected item didn't take focus, fine, but still want
185
                    // to make sure something else outside of the selected view
186
                    // has focus
187
                    final View focused = getFocusedChild();
188
                    if (focused != null) {
189
                        focused.clearFocus();
190
                    }
191
                    positionSelector(sel);
192
                } else {
193
                    sel.setSelected(false);
194
                    mSelectorRect.setEmpty();
195
                }
196
            } else {
197
                positionSelector(sel);
198
            }
199
            mSelectedTop = sel.getTop();
200
        } else {
201
            if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
202
                View child = getChildAt(mMotionPosition - mFirstPosition);
203
                if (child != null) positionSelector(child);
204
            } else {
205
                mSelectedTop = 0;
206
                mSelectorRect.setEmpty();
207
            }
208
            // even if there is not selected position, we may need to restore
209
            // focus (i.e. something focusable in touch mode)
210
            if (hasFocus() && focusLayoutRestoreView != null) {
211
                focusLayoutRestoreView.requestFocus();
212
            }
213
        }
214
        // tell focus view we are done mucking with it, if it is still in
215
        // our view hierarchy.
216
        if (focusLayoutRestoreView != null
217
                && focusLayoutRestoreView.getWindowToken() != null) {
218
            focusLayoutRestoreView.onFinishTemporaryDetach();
219
        }
220
        mLayoutMode = LAYOUT_NORMAL;
221
        mDataChanged = false;
222
        mNeedSync = false;
223
        setNextSelectedPositionInt(mSelectedPosition);
224
        updateScrollIndicators();
225
        if (mItemCount > 0) {
226
            checkSelectionChanged();
227
        }
228
        invokeOnItemScrollListener();
229
    } finally {
230
        if (!blockLayoutRequests) {
231
            mBlockLayoutRequests = false;
232
        }
233
    }
234
}

再来看看 fillFromTop 方法:

1
/**
2
 * Fills the list from top to bottom, starting with mFirstPosition
3
 * @param nextTop The location where the top of the first item should be drawn
4
 * @return The view that is currently selected
5
 */
6
private View fillFromTop(int nextTop) {
7
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
8
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
9
    if (mFirstPosition < 0) {
10
        mFirstPosition = 0;
11
    }
12
    // 我们看到,最终还是调用 fillDown 这个通用的函数(通用的意思是作为一个经常调用的方法,不依赖具体的情况)
13
    return fillDown(mFirstPosition, nextTop);
14
}

fillDown

1
/**
2
 * Fills the list from pos down to the end of the list view.
3
 * @param pos The first position to put in the list
4
 * @param nextTop The location where the top of the item associated with posshould be drawn
5
 * @return The view that is currently selected, if it happens to be in the range that we draw.
6
 */
7
private View fillDown(int pos, int nextTop) {
8
    View selectedView = null;
9
    int end = (getBottom() - getTop()) - mListPadding.bottom;
10
                    
11
    //不断地 makeAndAddView,直到下一个子View的Top不在屏幕中(也就是它不该被显示出来)或者数据已经到头了
12
    while (nextTop < end && pos < mItemCount) {
13
        // is this the selected item?
14
        boolean selected = pos == mSelectedPosition;
15
        
16
        // 这个 makeAndAddView就是下一步要执行的核心方法了
17
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
18
        //根据返回的child决定它的 nextTop
19
        nextTop = child.getBottom() + mDividerHeight;
20
        if (selected) {
21
            selectedView = child;
22
        }
23
        pos++;
24
    }
25
    return selectedView;
26
}

makeAndAddView

1
/**
2
 * Obtain the view and add it to our list of children. The view can be made
3
 * fresh, converted from an unused view, or used as is if it was in the
4
 * recycle bin.
5
 */
6
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
7
        boolean selected) {
8
    View child;
9
    if (!mDataChanged) {
10
        // Try to use an exsiting view for this position
11
        child = mRecycler.getActiveView(position);
12
        if (child != null) {
13
            // Found it -- we're using an existing child
14
            // This just needs to be positioned
15
-----
16
            setupChild(child, position, y, flow, childrenLeft, selected, true);
17
            return child;
18
        }
19
    }
20
    // Make a new view for this position, or convert an unused view if possible
21
    child = obtainView(position, mIsScrap);
22
    // This needs to be positioned and measured
23
-----
24
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
25
    return child;
26
}

这个方法主要就是获得一个View(有一个优先顺序,也就是先判断 mActiveVies 里面有没有 ; 没有就走 obtainView 方法,这个方法是一定能获取到View 的,它会先判断 mScrapView 是否可以返回一个View,没有的话就会调用 adapter.getView 并传入 convertView = null 以 inflate 一个新的 View) ,并且 将其传入 setupChild() (setupChild 方法的作用就是将这个获取到的子 View 加入到 ListView 中),由于我们前面的 fillDown 方法的循环,最终会不断地加入子View直到填满。

第二次 Layout

第二次的Layout仍然会继续从layoutChildern()开始。但相比较于第一次childCount不为0,那么相应的下面的逻辑也就发生变化了。我们在第一次layout的时候,调用过一个 fillActiveViews 方法,已经将 ActivieViews 都保存了,所以不用担心第二次 Layouyt的时候 View 不能重用的问题了。

我们可以做些什么: [RecyclerListener]

1
/**
2
   * A RecyclerListener is used to receive a notification whenever a View is placed
3
   * inside the RecycleBin's scrap heap. This listener is used to free resources
4
   * associated to Views placed in the RecycleBin.
5
   * android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
6
   */
7
  public static interface RecyclerListener {
8
      /**
9
       * Indicates that the specified View was moved into the recycler's scrap heap.
10
       * The view is not displayed on screen any more and any expensive resource
11
       * associated with the view should be discarded.
12
       * @param view
13
       */
14
      void onMovedToScrapHeap(View view);
15
  }

我们可以在 onMoveToScrapHeap 中做一些资源回收的操作。

Thanks:

  1. 源码解析ListView中的RecycleBin机制
  2. ListView工作原理完全解析
CATALOG
  1. 1. 前言
  2. 2. 带着问题来思考
  3. 3. ViewHolder
  4. 4. RecyclerView.Adapter
  5. 5. 一个简单的多ItemType的Adapter的实现
  6. 6. 多ItemType关于数据源的处理
    1. 6.0.0.1. 采用原始泛型的思想
    2. 6.0.0.2. 实现一个Model接口
    3. 6.0.0.3. 关于Header与Footer
      1. 6.0.0.3.1. 如何添加Header与Footer
  • 7. ListView的几个需要注意的地方
    1. 7.1. OnIntemSelectedListener
    2. 7.2. 多 ItemType 的Type定义的问题
  • 8. ListView源码分析
    1. 8.1. Adapter的作用
    2. 8.2. RecycleBin 机制
      1. 8.2.0.1. mActiveViews[] vs mScrapVies[]
      2. 8.2.0.2. FillActiveViews
      3. 8.2.0.3. RecycleBin 的基本原理
  • 8.3. ListView # onLayout
    1. 8.3.0.1. 第一次 Layout:
    2. 8.3.0.2. 第二次 Layout
    3. 8.3.0.3. 我们可以做些什么: [RecyclerListener]
    4. 8.3.0.4. Thanks: