Android Tech And Thoughts.

ListView与RecyclerView用法

Word count: 6.8kReading time: 32 min
2019/11/30 15 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工作原理完全解析
Powered By Valine
v1.5.2
CATALOG
  1. 1. 前言
  2. 2. 带着问题来思考
  3. 3. ViewHolder
  4. 4. RecyclerView.Adapter
  5. 5. 一个简单的多ItemType的Adapter的实现
  6. 6. 多ItemType关于数据源的处理
  • 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: