深入理解ListView工作原理(一)

前言

ListView——列表,它作为一个非常重要的显示方式,不管是在Web中还是在移动平台中,都是一个非常好的、不可或缺的展示信息的工具。在android中,ListView控件接管了这一重担,在大量的场合下,我们都需要使用这个控件。虽然在5.X时代,RecyclerView在很多地方逐渐取代了ListView,但ListView的使用范围还是非常广泛。

ListView显示需要三要素:

  • ListView是用来展示列表的View。
  • 适配器Adapter是用来把数据映射到ListView上的中介。
  • 数据是具体的将要被映射的字符串、图片或者基本组件。

ListView还有一个神奇的功能,相信大多数人都体验过,即使在ListView中加载非常多的数据,ListView都不会发生OOM或者奔溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长,这里涉及到ListView的视图缓存机制。

ListView的继承结构如下。

ListView的继承结构图

可以看到,ListView的继承结构还是相当复杂的,它是直接继承自AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,因此从这一点可以猜出ListView和GridView在工作原理和实现上都是有很多共同点的。然后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup,最后再到View和Object。

Adapter的作用

归根结底,一个View控件就是为了交互和展示数据用的,只不过ListView更加特殊,它是为了展示很多数据用的,但是ListView只承担交互和展示工作而已,至于这些数据来自哪里,ListView并不关心。因此,我们能设想到的最基本的ListView工作模式就是要有一个ListView控件和一个数据源。

不过如果真的让ListView和数据源直接打交道的话,那ListView所要做的适配工作就非常复杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据而已,至于这个数据源到底是什么类型,并没有严格的定义,有可能是数组或集合,甚至有可能是数据库表中查询出来的游标。所以说如果让ListView为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加,二是超出了它本身应该负责的工作范围,不再是仅仅承担交互和展示工作就可以了,这样ListView就会变得很臃肿。

于是就有了Adapter这样一个机制的出现。Adapter在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前接口不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口,它可以去实现各种各样的子类,每个子类都能通过自己的逻辑去完成特定的功能,以及与特定数据源的适配操作,比如ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示。

当然Adapter的作用不仅仅只有数据源适配这一点,还有一个非常重要的方法也需要我们在Adapter当中去重写,就是getView()方法。

RecyclerBin机制

在开始分析ListView源码之前,有一个东西是我们需要提前了解的,就是RecyclerBin机制,这个机制也是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。RecyclerBin的代码并不多,它是写在AbsListView中的一个内部类。先来看一下RecyclerBin中的主要代码实现。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
class RecycleBin {
private RecyclerListener mRecyclerListener;
private int mFirstActivePosition;
private View[] mActiveViews = new View[0];
private ArrayList<View>[] mScrapViews;
private int mViewTypeCount;
private ArrayList<View> mCurrentScrap;
private ArrayList<View> mSkippedScrap;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
public void markChildrenDirty() {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
final int scrapCount = scrap.size();
for (int i = 0; i < scrapCount; i++) {
scrap.get(i).forceLayout();
}
} else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
scrap.get(j).forceLayout();
}
}
}
if (mTransientStateViews != null) {
final int count = mTransientStateViews.size();
for (int i = 0; i < count; i++) {
mTransientStateViews.valueAt(i).forceLayout();
}
}
if (mTransientStateViewsById != null) {
final int count = mTransientStateViewsById.size();
for (int i = 0; i < count; i++) {
mTransientStateViewsById.valueAt(i).forceLayout();
}
}
}
public boolean shouldRecycleViewType(int viewType) {
return viewType >= 0;
}
void clear() {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
clearScrap(scrap);
} else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
clearScrap(scrap);
}
}
clearTransientStateViews();
}
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}
View getTransientStateView(int position) {
if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
long id = mAdapter.getItemId(position);
View result = mTransientStateViewsById.get(id);
mTransientStateViewsById.remove(id);
return result;
}
if (mTransientStateViews != null) {
final int index = mTransientStateViews.indexOfKey(position);
if (index >= 0) {
View result = mTransientStateViews.valueAt(index);
mTransientStateViews.removeAt(index);
return result;
}
}
return null;
}
void clearTransientStateViews() {
final SparseArray<View> viewsByPos = mTransientStateViews;
if (viewsByPos != null) {
final int N = viewsByPos.size();
for (int i = 0; i < N; i++) {
removeDetachedView(viewsByPos.valueAt(i), false);
}
viewsByPos.clear();
}
final LongSparseArray<View> viewsById = mTransientStateViewsById;
if (viewsById != null) {
final int N = viewsById.size();
for (int i = 0; i < N; i++) {
removeDetachedView(viewsById.valueAt(i), false);
}
viewsById.clear();
}
}
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.scrappedFromPosition = position;
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
scrap.dispatchStartTemporaryDetach();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
private ArrayList<View> getSkippedScrap() {
if (mSkippedScrap == null) {
mSkippedScrap = new ArrayList<>();
}
return mSkippedScrap;
}
void removeSkippedScrap() {
if (mSkippedScrap == null) {
return;
}
final int count = mSkippedScrap.size();
for (int i = 0; i < count; i++) {
removeDetachedView(mSkippedScrap.get(i), false);
}
mSkippedScrap.clear();
}
void scrapActiveViews() {
final View[] activeViews = mActiveViews;
final boolean hasListener = mRecyclerListener != null;
final boolean multipleScraps = mViewTypeCount > 1;
ArrayList<View> scrapViews = mCurrentScrap;
final int count = activeViews.length;
for (int i = count - 1; i >= 0; i--) {
final View victim = activeViews[i];
if (victim != null) {
final AbsListView.LayoutParams lp
= (AbsListView.LayoutParams) victim.getLayoutParams();
final int whichScrap = lp.viewType;
activeViews[i] = null;
if (victim.hasTransientState()) {
victim.dispatchStartTemporaryDetach();
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<View>();
}
long id = mAdapter.getItemId(mFirstActivePosition + i);
mTransientStateViewsById.put(id, victim);
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<View>();
}
mTransientStateViews.put(mFirstActivePosition + i, victim);
} else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(victim, false);
}
} else if (!shouldRecycleViewType(whichScrap)) {
if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(victim, false);
}
} else {
if (multipleScraps) {
scrapViews = mScrapViews[whichScrap];
}
lp.scrappedFromPosition = mFirstActivePosition + i;
removeDetachedView(victim, false);
scrapViews.add(victim);
if (hasListener) {
mRecyclerListener.onMovedToScrapHeap(victim);
}
}
}
}
pruneScrapViews();
}
void fullyDetachScrapViews() {
final int viewTypeCount = mViewTypeCount;
final ArrayList<View>[] scrapViews = mScrapViews;
for (int i = 0; i < viewTypeCount; ++i) {
final ArrayList<View> scrapPile = scrapViews[i];
for (int j = scrapPile.size() - 1; j >= 0; j--) {
final View view = scrapPile.get(j);
if (view.isTemporarilyDetached()) {
removeDetachedView(view, false);
}
}
}
}
private void pruneScrapViews() {
final int maxViews = mActiveViews.length;
final int viewTypeCount = mViewTypeCount;
final ArrayList<View>[] scrapViews = mScrapViews;
for (int i = 0; i < viewTypeCount; ++i) {
final ArrayList<View> scrapPile = scrapViews[i];
int size = scrapPile.size();
while (size > maxViews) {
scrapPile.remove(--size);
}
}
final SparseArray<View> transViewsByPos = mTransientStateViews;
if (transViewsByPos != null) {
for (int i = 0; i < transViewsByPos.size(); i++) {
final View v = transViewsByPos.valueAt(i);
if (!v.hasTransientState()) {
removeDetachedView(v, false);
transViewsByPos.removeAt(i);
i--;
}
}
}
final LongSparseArray<View> transViewsById = mTransientStateViewsById;
if (transViewsById != null) {
for (int i = 0; i < transViewsById.size(); i++) {
final View v = transViewsById.valueAt(i);
if (!v.hasTransientState()) {
removeDetachedView(v, false);
transViewsById.removeAt(i);
i--;
}
}
}
}
void reclaimScrapViews(List<View> views) {
if (mViewTypeCount == 1) {
views.addAll(mCurrentScrap);
} else {
final int viewTypeCount = mViewTypeCount;
final ArrayList<View>[] scrapViews = mScrapViews;
for (int i = 0; i < viewTypeCount; ++i) {
final ArrayList<View> scrapPile = scrapViews[i];
views.addAll(scrapPile);
}
}
}
void setCacheColorHint(int color) {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
final int scrapCount = scrap.size();
for (int i = 0; i < scrapCount; i++) {
scrap.get(i).setDrawingCacheBackgroundColor(color);
}
} else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
scrap.get(j).setDrawingCacheBackgroundColor(color);
}
}
}
final View[] activeViews = mActiveViews;
final int count = activeViews.length;
for (int i = 0; i < count; ++i) {
final View victim = activeViews[i];
if (victim != null) {
victim.setDrawingCacheBackgroundColor(color);
}
}
}
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =
(AbsListView.LayoutParams) view.getLayoutParams();
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}
private void clearScrap(final ArrayList<View> scrap) {
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
}
}
private void clearAccessibilityFromScrap(View view) {
view.clearAccessibilityFocus();
view.setAccessibilityDelegate(null);
}
private void removeDetachedView(View child, boolean animate) {
child.setAccessibilityDelegate(null);
AbsListView.this.removeDetachedView(child, animate);
}
}

接下来先简单解读一下RecycleBin最主要的几个方法。

  • fillActiveViews(int childCount, int firstActivePosition)这个方法接收两个参数,第一个参数标识要存储的view的数量,第二个参数标识ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中指定的元素存储到mActiveViews数组中。
  • View getActiveView(int position)这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
  • void addScrapView(View scrap, int position)用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
  • View getScrapView(int position)用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
  • void setViewTypeCount(int viewTypeCount)。我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。

ListView的布局工作原理

ListView即使再特殊最终还是继承自View的,因此它的执行流程还会按照View的规则来执行。View的执行流程无非就分为3步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在listView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了,因此接下来主要分析这个方法的工作原理。

ListView的onLayout()方法是在父类AbsListView中实现的,源码实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}

可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。再看到layoutChildren()这个方法,这个方法是用来进行子元素布局的,但是这个方法是个空方法,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类来完成的。所以,进入ListView的layoutChildren()方法进一步分析。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
index = mNextSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
newSel = getChildAt(index);
}
break;
case LAYOUT_FORCE_TOP:
case LAYOUT_FORCE_BOTTOM:
case LAYOUT_SPECIFIC:
case LAYOUT_SYNC:
break;
case LAYOUT_MOVE_SELECTION:
default:
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
setSelectedPositionInt(mNextSelectedPosition);
AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
View accessibilityFocusLayoutRestoreView = null;
int accessibilityFocusPosition = INVALID_POSITION;
final ViewRootImpl viewRootImpl = getViewRootImpl();
if (viewRootImpl != null) {
final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
if (focusHost != null) {
final View focusChild = getAccessibilityFocusedChild(focusHost);
if (focusChild != null) {
if (!dataChanged || isDirectChildHeaderOrFooter(focusChild)
|| focusChild.hasTransientState() || mAdapterHasStableIds) {
accessibilityFocusLayoutRestoreView = focusHost;
accessibilityFocusLayoutRestoreNode = viewRootImpl
.getAccessibilityFocusedVirtualView();
}
accessibilityFocusPosition = getPositionForView(focusChild);
}
}
}
View focusLayoutRestoreDirectChild = null;
View focusLayoutRestoreView = null;
final View focusedChild = getFocusedChild();
if (focusedChild != null) {
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)
|| focusedChild.hasTransientState() || mAdapterHasStableIds) {
focusLayoutRestoreDirectChild = focusedChild;
focusLayoutRestoreView = findFocus();
if (focusLayoutRestoreView != null) {
focusLayoutRestoreView.dispatchStartTemporaryDetach();
}
}
requestFocus();
}
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
final int selectedPosition = reconcileSelectedPosition();
sel = fillSpecific(selectedPosition, mSpecificTop);
if (sel == null && mFocusSelector != null) {
final Runnable focusRunnable = mFocusSelector
.setupFocusIfValid(selectedPosition);
if (focusRunnable != null) {
post(focusRunnable);
}
}
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
recycleBin.scrapActiveViews();
removeUnusedFixedViews(mHeaderViewInfos);
removeUnusedFixedViews(mFooterViewInfos);
if (sel != null) {
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
focusLayoutRestoreView != null &&
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
if (!focusWasTaken) {
final View focused = getFocusedChild();
if (focused != null) {
focused.clearFocus();
}
positionSelector(INVALID_POSITION, sel);
} else {
sel.setSelected(false);
mSelectorRect.setEmpty();
}
} else {
positionSelector(INVALID_POSITION, sel);
}
mSelectedTop = sel.getTop();
} else {
final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP
|| mTouchMode == TOUCH_MODE_DONE_WAITING;
if (inTouchMode) {
final View child = getChildAt(mMotionPosition - mFirstPosition);
if (child != null) {
positionSelector(mMotionPosition, child);
}
} else if (mSelectorPosition != INVALID_POSITION) {
final View child = getChildAt(mSelectorPosition - mFirstPosition);
if (child != null) {
positionSelector(mSelectorPosition, child);
}
} else {
mSelectedTop = 0;
mSelectorRect.setEmpty();
}
if (hasFocus() && focusLayoutRestoreView != null) {
focusLayoutRestoreView.requestFocus();
}
}
if (viewRootImpl != null) {
final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
if (newAccessibilityFocusedView == null) {
if (accessibilityFocusLayoutRestoreView != null
&& accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
final AccessibilityNodeProvider provider =
accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
accessibilityFocusLayoutRestoreNode.getSourceNodeId());
provider.performAction(virtualViewId,
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
} else {
accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
}
} else if (accessibilityFocusPosition != INVALID_POSITION) {
final int position = MathUtils.constrain(
accessibilityFocusPosition - mFirstPosition, 0,
getChildCount() - 1);
final View restoreView = getChildAt(position);
if (restoreView != null) {
restoreView.requestAccessibilityFocus();
}
}
}
}
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.dispatchFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}

ListView第一次layout过程中,ListView当中还没有任何子View,数据都还是由Adapter管理的,并没有展示到界面上,因此getChildCount()方法得到的值是0,接着会根据dataChanged这个布尔值来进行判断执行逻辑,dataChanged只有在数据源发生改变的情况下才会变成true,其他情况下都是false,因此,会进入RecycleBin的fillActiveViews()方法,这个方法调用是为了将ListView的子View进行缓存的,但是目前ListView中还没有任何子View,因此这一步暂时还起不了作用。

接下来会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到default语句当中,紧接着进行2次if判断,childCount目前是0,并且默认的布局顺序是从上往下,因此会进入到fillFrimTop()方法。

1
2
3
4
5
6
7
8
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
return fillDown(mFirstPosition, nextTop);
}

这个方法所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法本身没有什么逻辑,就是判断一下mFirstPosition值的合法性,然后调用fillDown()方法,因此,填充ListView的操作是在fillDown()方法中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}

可以看到,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所有的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end的值,并且pos也是小于mItemCount的值的。那么每执行一次while循环,pos的值就会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都会被遍历结束了,就会跳出while循环。

在while循环中,最值得留意的方法就是makeAndAddView()方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
final View child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

这个方法一开始尝试从RecycleBin当中去快速获取一个active view,不过目前RecycleBin当中还没有缓存任何View,所以这里得到的值是null。得到null后会继续向下运行,调用obtainView()方法来再次尝试获取一个View,这个方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中。先来看看obtainView()方法的实现。

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
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
outMetadata[0] = false;
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
outMetadata[0] = true;
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
setItemViewLayoutParams(child, position);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}

obtainView()方法在第一次layout的时候会调用RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,这里会返回null,于是紧接着会调用mAdapter的getView()方法来获取一个View。这里mAdapter就是与ListView关联的适配器,而getView()方法就是我们最为熟悉的在适配器里面的的重载方法。getView()方法接受的三个参数,第一个参数position代表当前子元素的位置,可以通过具体的位置来获取与其相关的数据,第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法去加载一个布局,接下来就是对这个view进行一些属性和值的设定,最后将view返回。

这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
final boolean isSelected = selected && shouldShowSelector();
final boolean updateChildSelected = isSelected != child.isSelected();
final int mode = mTouchMode;
final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
&& mMotionPosition == position;
final boolean updateChildPressed = isPressed != child.isPressed();
final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
|| child.isLayoutRequested();
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
if (child instanceof Checkable) {
((Checkable) child).setChecked(mCheckStates.get(position));
} else if (getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
child.setActivated(mCheckStates.get(position));
}
}
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position) {
child.jumpDrawablesToCurrentState();
}
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
child.resolveRtlPropertiesIfNeeded();
}
if (needToMeasure) {
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

setupChild()方法当中的代码虽然比较多,但是核心代码不多。刚才调用的obtainView()方法获取到的子元素View,最终会调用addViewInLayout()方法将它添加到ListView当中。

根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满后就跳出,也就是说即使Adapter中有一千多条数据,ListView也只会加载满第一屏的数据,剩下的数据目前在屏幕上看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上了。到此,ListView的第一次layout过程结束。

我们知道,View初始化时会measure三次,layout两次,draw一次,这是一个很小的细节,平时影响并不大,因为不管测量布局几次,都是执行相同的逻辑,所以并不予以过多关心。但是在ListView中情况就不一样了,因为这意味着layouChildren()过程会执行两次,而这个过程当涉及向ListView中添加子元素,如果相同的逻辑执行两遍的话,ListView中就会存在一份重复的数据了。因此ListView在layoutChildren()过程中做了第二次layout的逻辑处理,巧妙地解决了这个问题。下面开始分析第二次layout的过程。

ListView第二次layout过程中,调用getChildCount()方法来获取子View的数量,这一次得到的值不再为0,而是ListView中一屏可以显示的子View数量。紧接着调用RecycleBin的fillActiveViews()方法,因为目前ListView中已经有子View了,这样所有的子View都会被缓存到RecycleBin的mActiveViews数组当中。接着是一个非常重要的操作,即调用detachAllViewsFromParent()方法将所有ListView中的子View清除掉,从而保证第二次layout过程不会产生一份重复的数据,因为之前调用了RecycleBin的fillActiveViews()方法来缓存子View,所以第二次重新加载直接使用这些缓存好的View就可以了,并不会重新执行一次inflate过程,因此效率方面并不会有明显的影响。

第二次layout过程中,由于getChildCount()不为0,所以取代fillDown()或fillUp()方法的是fillSpecific()方法,这个方法与前两个的功能也是差不多的,主要区别在于,fillSpecial()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。这里不再去关注太多细节,而是将精力放回makeAndAddView()方法来。makeAndAddView()方法仍然尝试从RecycleBin当中获取Active View,然而这次就可以取到了,所以就不用进入obtainView()方法,而是直接进入setupChild()方法当中,这样也会节省了很多时间,提高了效率。setupChild()方法也不再调用addViewInLayout()方法,而是改调用attachViewToParent()方法将在前面被detachAllViewsFromParent()方法置为detach状态的子View重新变为attach状态的,最终ListView中所有的子View又都可以正常显示出来了,至此ListView的第二次layout就结束了。

ListView滑动加载更多数据工作原理

经过前面分析的两次layout过程,我们已经可以在ListView中看到内容了,但是目前ListView中只是加载并显示了第一屏的数据而已。因此,接下来就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。

由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码是写在AbsListView当中的,所以接下来看一下AbsListView中监听触控事件的onTouchEvent()方法。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
return isClickable() || isLongClickable();
}
if (mPositionScroller != null) {
mPositionScroller.stop();
}
if (mIsDetaching || !isAttachedToWindow()) {
return false;
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
return true;
}
initVelocityTrackerIfNotExists();
final MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
onTouchDown(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
case MotionEvent.ACTION_UP: {
onTouchUp(ev);
break;
}
case MotionEvent.ACTION_CANCEL: {
onTouchCancel();
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);
final int x = mMotionX;
final int y = mMotionY;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
final View child = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = child.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
final int id = ev.getPointerId(index);
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
mMotionCorrection = 0;
mActivePointerId = id;
mMotionX = x;
mMotionY = y;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
final View child = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = child.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}

因为目前所关心的就只有手指在屏幕上滑动这一事件而已,所以这里只需要关注ACTION_MOVE这个动作。ACTION_MOVE这个case里面调用了onTouchMove()方法。

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
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
if (mHasPerformedLongPress) {
return;
}
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
if (mDataChanged) {
layoutChildren();
}
final int y = (int) ev.getY(pointerIndex);
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
break;
}
final View motionView = getChildAt(mMotionPosition - mFirstPosition);
final float x = ev.getX(pointerIndex);
if (!pointInView(x, y, mTouchSlop)) {
setPressed(false);
if (motionView != null) {
motionView.setPressed(false);
}
removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
mPendingCheckForTap : mPendingCheckForLongPress);
mTouchMode = TOUCH_MODE_DONE_WAITING;
updateSelectorState();
} else if (motionView != null) {
final float[] point = mTmpPoint;
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, motionView);
motionView.drawableHotspotChanged(point[0], point[1]);
}
break;
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}

当手指在屏幕上滑动的时候,又会进入TOUCH_MODE_SCROLL这个动作,这个动作下又会调用scrollIfNeeded()方法。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
int rawDeltaY = y - mMotionY;
int scrollOffsetCorrection = 0;
int scrollConsumedCorrection = 0;
if (mLastY == Integer.MIN_VALUE) {
rawDeltaY -= mMotionCorrection;
}
if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY,
mScrollConsumed, mScrollOffset)) {
rawDeltaY += mScrollConsumed[1];
scrollOffsetCorrection = -mScrollOffset[1];
scrollConsumedCorrection = mScrollConsumed[1];
if (vtev != null) {
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
}
final int deltaY = rawDeltaY;
int incrementalDeltaY =
mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
int lastYCorrection = 0;
if (mTouchMode == TOUCH_MODE_SCROLL) {
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("AbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");
}
if (y != mLastY) {
if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
Math.abs(rawDeltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
final int motionIndex;
if (mMotionPosition >= 0) {
motionIndex = mMotionPosition - mFirstPosition;
} else {
motionIndex = getChildCount() / 2;
}
int motionViewPrevTop = 0;
View motionView = this.getChildAt(motionIndex);
if (motionView != null) {
motionViewPrevTop = motionView.getTop();
}
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
motionView = this.getChildAt(motionIndex);
if (motionView != null) {
final int motionViewRealTop = motionView.getTop();
if (atEdge) {
int overscroll = -incrementalDeltaY -
(motionViewRealTop - motionViewPrevTop);
if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll,
mScrollOffset)) {
lastYCorrection -= mScrollOffset[1];
if (vtev != null) {
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
} else {
final boolean atOverscrollEdge = overScrollBy(0, overscroll,
0, mScrollY, 0, 0, 0, mOverscrollDistance, true);
if (atOverscrollEdge && mVelocityTracker != null) {
mVelocityTracker.clear();
}
final int overscrollMode = getOverScrollMode();
if (overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
!contentFits())) {
if (!atOverscrollEdge) {
mDirection = 0;
mTouchMode = TOUCH_MODE_OVERSCROLL;
}
if (incrementalDeltaY > 0) {
mEdgeGlowTop.onPull((float) -overscroll / getHeight(),
(float) x / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
invalidateTopGlow();
} else if (incrementalDeltaY < 0) {
mEdgeGlowBottom.onPull((float) overscroll / getHeight(),
1.f - (float) x / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
invalidateBottomGlow();
}
}
}
}
mMotionY = y + lastYCorrection + scrollOffsetCorrection;
}
mLastY = y + lastYCorrection + scrollOffsetCorrection;
}
} else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
...
}
}

scrollIfNeeded()这个方法通过计算incrementalDeltaY的正负情况来判断用户是向上还是向下滑动的(如果小于0标识向下滑动,否则就是向上滑动),并且会进行边界值检测的过程,主要是通过调用trackMotionScroll()这个方法来实现的。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();
final Rect listPadding = mListPadding;
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = listPadding.top;
effectivePaddingBottom = listPadding.bottom;
}
final int spaceAbove = effectivePaddingTop - firstTop;
final int end = getHeight() - effectivePaddingBottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - mPaddingBottom - mPaddingTop;
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
if (firstPosition == 0) {
mFirstPositionDistanceGuess = firstTop - listPadding.top;
} else {
mFirstPositionDistanceGuess += incrementalDeltaY;
}
if (firstPosition + childCount == mItemCount) {
mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
} else {
mLastPositionDistanceGuess += incrementalDeltaY;
}
final boolean cannotScrollDown = (firstPosition == 0 &&
firstTop >= listPadding.top && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);
if (cannotScrollDown || cannotScrollUp) {
return incrementalDeltaY != 0;
}
final boolean down = incrementalDeltaY < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
int bottom = getHeight() - incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
bottom -= listPadding.bottom;
}
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
if (!awakenScrollBars()) {
invalidate();
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
mRecycler.fullyDetachScrapViews();
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(mSelectedPosition, getChildAt(childIndex));
}
} else if (mSelectorPosition != INVALID_POSITION) {
final int childIndex = mSelectorPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(INVALID_POSITION, getChildAt(childIndex));
}
} else {
mSelectorRect.setEmpty();
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
return false;
}

这个方法会在ListView向下滑动的时候,进入一个for循环,从上到下一次获取子View,如果该子View的bottom已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。之后会根据计算器的值来进行一次detach操作。紧接着调用offsetChildrenTopAndBottom()并将incrementalDeltaY作为参数传入,这个方法让ListView中所有的子View都按照传入的参数值进行相对的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。如果ListView中最后一个View的底部已经移入了屏幕,或者第一个View的顶部移入了屏幕,就会调用fillGap()方法用来加载屏幕外的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}

fillGap()这个方法通过down参数判断ListView是向下滑动还是向上滑动,并相应调用fillDown()或fillUp()方法。这两个方法前面已经分析过,内部是通过循环对ListView进行填充,循环的主要逻辑就是makeAndAddView()方法,这时候这个方法会调用RecycleBin的getScrapView()方法从废弃缓存中获取一个View。

这样它们之间就形成了一个生产者和消费者的模式,不管有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移如屏幕的数据重新利用起来,因此不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

最后附上一张图来总结一下ListView滑动加载数据的工作原理。