在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。
另外ListView还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么ListView是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把ListView的源码通读了一遍,基本了解了它的工作原理,在感叹Google大神能够写出如此精妙代码的同时我也有所敬畏,因为ListView的代码量比较大,复杂度也很高,很难用文字表达清楚,于是我就放弃了把它写成一篇博客的想法。那么现在回想起来这件事我已经肠子都悔青了,因为没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。于是现在我又重新定下心来再次把ListView的源码重读了一遍,那么这次我一定要把它写成一篇博客,分享给大家的同时也当成我自己的笔记吧。
首先我们先来看一下ListView的继承结构,如下图所示:
可以看到,ListView的继承结构还是相当复杂的,它是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,因此我们从这一点就可以猜出来,ListView和GridView在工作原理和实现上都是有很多共同点的。然后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup,后面就是我们所熟知的了。先把ListView的继承结构了解一下,待会儿有助于我们更加清晰地分析代码。
Adapter的作用
Adapter相信大家都不会陌生,我们平时使用ListView的时候一定都会用到它。那么话说回来大家有没有仔细想过,为什么需要Adapter这个东西呢?总感觉正因为有了Adapter,ListView的使用变得要比其它控件复杂得多。那么这里我们就先来学习一下Adapter到底起到了什么样的一个作用。
其实说到底,控件就是为了交互和展示数据用的,只不过ListView更加特殊,它是为了展示很多很多数据用的,但是ListView只承担交互和展示工作而已,至于这些数据来自哪里,ListView是不关心的。因此,我们能设想到的最基本的ListView工作模式就是要有一个ListView控件和一个数据源。
不过如果真的让ListView和数据源直接打交道的话,那ListView所要做的适配工作就非常繁杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据而已,至于这个数据源到底是什么样类型,并没有严格的定义,有可能是数组,也有可能是集合,甚至有可能是数据库表中查询出来的游标。所以说如果ListView真的去为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它本身应该负责的工作范围,不再是仅仅承担交互和展示工作就可以了,这样ListView就会变得比较臃肿。
那么显然Android开发团队是不会允许这种事情发生的,于是就有了Adapter这样一个机制的出现。顾名思义,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示:
当然Adapter的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我们在Adapter当中去重写,就是getView()方法,这个在下面的文章中还会详细讲到。
RecycleBin机制
那么在开始分析ListView的源码之前,还有一个东西是我们提前需要了解的,就是RecycleBin机制,这个机制也是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。其实RecycleBin的代码并不多,只有300行左右,它是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。那我们来看一下RecycleBin中的主要代码,如下所示:
001.
/**
002.
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin
003.
* has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
004.
* those views which were onscreen at the start of a layout. By
005.
* construction, they are displaying current information. At the end of
006.
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
007.
* are old views that could potentially be used by the adapter to avoid
008.
* allocating views unnecessarily.
009.
*
010.
* @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
011.
* @see android.widget.AbsListView.RecyclerListener
012.
*/
013.
class
RecycleBin {
014.
private
RecyclerListener mRecyclerListener;
015.
016.
/**
017.
* The position of the first view stored in mActiveViews.
018.
*/
019.
private
int
mFirstActivePosition;
020.
021.
/**
022.
* Views that were on screen at the start of layout. This array is
023.
* populated at the start of layout, and at the end of layout all view
024.
* in mActiveViews are moved to mScrapViews. Views in mActiveViews
025.
* represent a contiguous range of Views, with position of the first
026.
* view store in mFirstActivePosition.
027.
*/
028.
private
View[] mActiveViews =
new
View[
0
];
029.
030.
/**
031.
* Unsorted views that can be used by the adapter as a convert view.
032.
*/
033.
private
ArrayList<View>[] mScrapViews;
034.
035.
private
int
mViewTypeCount;
036.
037.
private
ArrayList<View> mCurrentScrap;
038.
039.
/**
040.
* Fill ActiveViews with all of the children of the AbsListView.
041.
*
042.
* @param childCount
043.
* The minimum number of views mActiveViews should hold
044.
* @param firstActivePosition
045.
* The position of the first view that will be stored in
046.
* mActiveViews
047.
*/
048.
void
fillActiveViews(
int
childCount,
int
firstActivePosition) {
049.
if
(mActiveViews.length < childCount) {
050.
mActiveViews =
new
View[childCount];
051.
}
052.
mFirstActivePosition = firstActivePosition;
053.
final
View[] activeViews = mActiveViews;
054.
for
(
int
i =
0
; i < childCount; i++) {
055.
View child = getChildAt(i);
056.
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
057.
// Don't put header or footer views into the scrap heap
058.
if
(lp !=
null
&& lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
059.
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
060.
// active views.
061.
// However, we will NOT place them into scrap views.
062.
activeViews[i] = child;
063.
}
064.
}
065.
}
066.
067.
/**
068.
* Get the view corresponding to the specified position. The view will
069.
* be removed from mActiveViews if it is found.
070.
*
071.
* @param position
072.
* The position to look up in mActiveViews
073.
* @return The view if it is found, null otherwise
074.
*/
075.
View getActiveView(
int
position) {
076.
int
index = position - mFirstActivePosition;
077.
final
View[] activeViews = mActiveViews;
078.
if
(index >=
0
&& index < activeViews.length) {
079.
final
View match = activeViews[index];
080.
activeViews[index] =
null
;
081.
return
match;
082.
}
083.
return
null
;
084.
}
085.
086.
/**
087.
* Put a view into the ScapViews list. These views are unordered.
088.
*
089.
* @param scrap
090.
* The view to add
091.
*/
092.
void
addScrapView(View scrap) {
093.
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
094.
if
(lp ==
null
) {
095.
return
;
096.
}
097.
// Don't put header or footer views or views that should be ignored
098.
// into the scrap heap
099.
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代码并不全,我只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析ListView的工作原理将会有很大的帮助。
fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。 getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。 addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。 getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。 setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。了解了RecycleBin中的主要方法以及它们的用处之后,下面就可以开始来分析ListView的工作原理了,这里我将还是按照以前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,不然的话要是把ListView所有的代码都贴出来,那么本篇文章将会很长很长了。
第一次Layout
不管怎么说,ListView即使再特殊最终还是继承自View的,因此它的执行流程还将会按照View的规则来执行,对于这方面不太熟悉的朋友可以参考我之前写的 Android视图绘制流程完全解析,带你一步步深入了解View(二) 。
View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了,因此我们本篇文章也是主要分析的这个方法里的内容。
如果你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView中实现的,代码如下所示:
01.
/**
02.
* Subclasses should NOT override this method but {@link #layoutChildren()}
03.
* instead.
04.
*/
05.
@Override
06.
protected
void
onLayout(
boolean
changed,
int
l,
int
t,
int
r,
int
b) {
07.
super
.onLayout(changed, l, t, r, b);
08.
mInLayout =
true
;
09.
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.
}
001.
@Override
002.
protected
void
layoutChildren() {
003.
final
boolean
blockLayoutRequests = mBlockLayoutRequests;
004.
if
(!blockLayoutRequests) {
005.
mBlockLayoutRequests =
true
;
006.
}
else
{
007.
return
;
008.
}
009.
try
{
010.
super
.layoutChildren();
011.
invalidate();
012.
if
(mAdapter ==
null
) {
013.
resetList();
014.
invokeOnItemScrollListener();
015.
return
;
016.
}
017.
int
childrenTop = mListPadding.top;
018.
int
childrenBottom = getBottom() - getTop() - mListPadding.bottom;
019.
int
childCount = getChildCount();
020.
int
index =
0
;
021.
int
delta =
0
;
022.
View sel;
023.
View oldSel =
null
;
024.
View oldFirst =
null
;
025.
View newSel =
null
;
026.
View focusLayoutRestoreView =
null
;
027.
// Remember stuff we will need down below
028.
switch
(mLayoutMode) {
029.
case
LAYOUT_SET_SELECTION:
030.
index = mNextSelectedPosition - mFirstPosition;
031.
if
(index >=
0
&& index < childCount) {
032.
newSel = getChildAt(index);
033.
}
034.
break
;
035.
case
LAYOUT_FORCE_TOP:
036.
case
LAYOUT_FORCE_BOTTOM:
037.
case
LAYOUT_SPECIFIC:
038.
case
LAYOUT_SYNC:
039.
break
;
040.
case
LAYOUT_MOVE_SELECTION:
041.
default
:
042.
// Remember the previously selected view
043.
index = mSelectedPosition - mFirstPosition;
044.
if
(index >=
0
&& index < childCount) {
045.
oldSel = getChildAt(index);
046.
}
047.
// Remember the previous first child
048.
oldFirst = getChildAt(
0
);
049.
if
(mNextSelectedPosition >=
0
) {
050.
delta = mNextSelectedPosition - mSelectedPosition;
051.
}
052.
// Caution: newSel might be null
053.
newSel = getChildAt(index + delta);
054.
}
055.
boolean
dataChanged = mDataChanged;
056.
if
(dataChanged) {
057.
handleDataChanged();
058.
}
059.
// Handle the empty set by removing all views that are visible
060.
// and calling it a day
061.
if
(mItemCount ==
0
) {
062.
resetList();
063.
invokeOnItemScrollListener();
064.
return
;
065.
}
else
if
(mItemCount != mAdapter.getCount()) {
066.
throw
new
IllegalStateException(
"The content of the adapter has changed but "
067.
+
"ListView did not receive a notification. Make sure the content of "
068.
+
"your adapter is not modified from a background thread, but only "
069.
+
"from the UI thread. [in ListView("
+ getId() +
", "
+ getClass()
070.
+
") with Adapter("
+ mAdapter.getClass() +
")]"
);
071.
}
072.
setSelectedPositionInt(mNextSelectedPosition);
073.
// Pull all children into the RecycleBin.
074.
// These views will be reused if possible
075.
final
int
firstPosition = mFirstPosition;
076.
final
RecycleBin recycleBin = mRecycler;
077.
// reset the focus restoration
078.
View focusLayoutRestoreDirectChild =
null
;
079.
// Don't put header or footer views into the Recycler. Those are
080.
// already cached in mHeaderViews;
081.
if
(dataChanged) {
082.
for
(
int
i =
0
; i < childCount; i++) {
083.
recycleBin.addScrapView(getChildAt(i));
084.
if
(ViewDebug.TRACE_RECYCLER) {
085.
ViewDebug.trace(getChildAt(i),
086.
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
087.
}
088.
}
089.
}
else
{
090.
recycleBin.fillActiveViews(childCount, firstPosition);
091.
}
092.
// take focus back to us temporarily to avoid the eventual
093.
// call to clear focus when removing the focused child below
094.
// from messing things up when ViewRoot assigns focus back
095.
// to someone else
096.
final
View focusedChild = getFocusedChild();
097.
if
(focusedChild !=
null
) {
098.
// TODO: in some cases focusedChild.getParent() == null
099.
// we can remember the focused view to restore after relayout if the
100.
// data hasn't changed, or if the focused position is a header or footer
101.
if
(!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
102.
focusLayoutRestoreDirectChild = focusedChild;
103.
// remember the specific view that had focus
104.
focusLayoutRestoreView = findFocus();
105.
if
(focusLayoutRestoreView !=
null
) {
106.
// tell it we are going to mess with it
107.
focusLayoutRestoreView.onStartTemporaryDetach();
108.
}
109.
}
110.
requestFocus();
111.
}
112.
// Clear out old views
113.
detachAllViewsFromParent();
114.
switch
(mLayoutMode) {
115.
case
LAYOUT_SET_SELECTION:
116.
if
(newSel !=
null
) {
117.
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
118.
}
else
{
119.
sel = fillFromMiddle(childrenTop, childrenBottom);
120.
}
121.
break
;
122.
case
LAYOUT_SYNC:
123.
sel = fillSpecific(mSyncPosition, mSpecificTop);
124.
break
;
125.
case
LAYOUT_FORCE_BOTTOM:
126.
sel = fillUp(mItemCount -
1
, childrenBottom);
127.
adjustViewsUpOrDown();
128.
break
;
129.
case
LAYOUT_FORCE_TOP:
130.
mFirstPosition =
0
;
131.
sel = fillFromTop(childrenTop);
132.
adjustViewsUpOrDown();
133.
break
;
134.
case
LAYOUT_SPECIFIC:
135.
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
136.
break
;
137.
case
LAYOUT_MOVE_SELECTION:
138.
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
139.
break
;
140.
default
:
141.
if
(childCount ==
0
) {
142.
if
(!mStackFromBottom) {
143.
final
int
position = lookForSelectablePosition(
0
,
true
);
144.
setSelectedPositionInt(position);
145.
sel = fillFromTop(childrenTop);
146.
}
else
{
147.
final
int
position = lookForSelectablePosition(mItemCount -
1
,
false
);
148.
setSelectedPositionInt(position);
149.
sel = fillUp(mItemCount -
1
, childrenBottom);
150.
}
151.
}
else
{
152.
if
(mSelectedPosition >=
0
&& mSelectedPosition < mItemCount) {
153.
sel = fillSpecific(mSelectedPosition,
154.
oldSel ==
null
? childrenTop : oldSel.getTop());
155.
}
else
if
(mFirstPosition < mItemCount) {
156.
sel = fillSpecific(mFirstPosition,
157.
oldFirst ==
null
? childrenTop : oldFirst.getTop());
158.
}
else
{
159.
sel = fillSpecific(
0
, childrenTop);
160.
}
161.
}
162.
break
;
163.
}
164.
// Flush any cached views that did not get reused above
165.
recycleBin.scrapActiveViews();
166.
if
(sel !=
null
) {
167.
// the current selected item should get focus if items
168.
// are focusable
169.
if
(mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
170.
final
boolean
focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
171.
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
172.
if
(!focusWasTaken) {
173.
// selected item didn't take focus, fine, but still want
174.
// to make sure something else outside of the selected view
175.
// has focus
176.
final
View focused = getFocusedChild();
177.
if
(focused !=
null
) {
178.
focused.clearFocus();
179.
}
180.
positionSelector(sel);
181.
}
else
{
182.
sel.setSelected(
false
);
183.
mSelectorRect.setEmpty();
184.
}
185.
}
else
{
186.
positionSelector(sel);
187.
}
188.
mSelectedTop = sel.getTop();
189.
}
else
{
190.
if
(mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
191.
View child = getChildAt(mMotionPosition - mFirstPosition);
192.
if
(child !=
null
) positionSelector(child);
193.
}
else
{
194.
mSelectedTop =
0
;
195.
mSelectorRect.setEmpty();
196.
}
197.
// even if there is not selected position, we may need to restore
198.
// focus (i.e. something focusable in touch mode)
199.
if
(hasFocus() && focusLayoutRestoreView !=
null
) {
200.
focusLayoutRestoreView.requestFocus();
201.
}
202.
}
203.
// tell focus view we are done mucking with it, if it is still in
204.
// our view hierarchy.
205.
if
(focusLayoutRestoreView !=
null
206.
&& focusLayoutRestoreView.getWindowToken() !=
null
) {
207.
focusLayoutRestoreView.onFinishTemporaryDetach();
208.
}
209.
mLayoutMode = LAYOUT_NORMAL;
210.
mDataChanged =
false
;
211.
mNeedSync =
false
;
212.
setNextSelectedPositionInt(mSelectedPosition);
213.
updateScrollIndicators();
214.
if
(mItemCount >
0
) {
215.
checkSelectionChanged();
216.
}
217.
invokeOnItemScrollListener();
218.
}
finally
{
219.
if
(!blockLayoutRequests) {
220.
mBlockLayoutRequests =
false
;
221.
}
222.
}
223.
}
这段代码比较长,我们挑重点的看。首先可以确定的是,ListView当中目前还没有任何子View,数据都还是由Adapter管理的,并没有展示到界面上,因此第19行getChildCount()方法得到的值肯定是0。接着在第81行会根据dataChanged这个布尔型的值来判断执行逻辑,dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入到第90行的执行逻辑,调用RecycleBin的fillActiveViews()方法。按理来说,调用fillActiveViews()方法是为了将ListView的子View进行缓存的,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。
接下来在第114行会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到第140行的default语句当中。而下面又会紧接着进行两次if判断,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到第145行的fillFromTop()方法,我们跟进去瞧一瞧:
01.
/**
02.
* Fills the list from top to bottom, starting with mFirstPosition
03.
*
04.
* @param nextTop The location where the top of the first item should be
05.
* drawn
06.
*
07.
* @return The view that is currently selected
08.
*/
09.
private
View fillFromTop(
int
nextTop) {
10.
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
11.
mFirstPosition = Math.min(mFirstPosition, mItemCount -
1
);
12.
if
(mFirstPosition <
0
) {
13.
mFirstPosition =
0
;
14.
}
15.
return
fillDown(mFirstPosition, nextTop);
16.
}
01.
/**
02.
* Fills the list from pos down to the end of the list view.
03.
*
04.
* @param pos The first position to put in the list
05.
*
06.
* @param nextTop The location where the top of the item associated with pos
07.
* should be drawn
08.
*
09.
* @return The view that is currently selected, if it happens to be in the
10.
* range that we draw.
11.
*/
12.
private
View fillDown(
int
pos,
int
nextTop) {
13.
View selectedView =
null
;
14.
int
end = (getBottom() - getTop()) - mListPadding.bottom;
15.
while
(nextTop < end && pos < mItemCount) {
16.
// is this the selected item?
17.
boolean
selected = pos == mSelectedPosition;
18.
View child = makeAndAddView(pos, nextTop,
true
, mListPadding.left, selected);
19.
nextTop = child.getBottom() + mDividerHeight;
20.
if
(selected) {
21.
selectedView = child;
22.
}
23.
pos++;
24.
}
25.
return
selectedView;
26.
}
可以看到,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。
那么while循环当中又做了什么事情呢?值得让人留意的就是第18行调用的makeAndAddView()方法,进入到这个方法当中,代码如下所示:
01.
/**
02.
* Obtain the view and add it to our list of children. The view can be made
03.
* fresh, converted from an unused view, or used as is if it was in the
04.
* recycle bin.
05.
*
06.
* @param position Logical position in the list
07.
* @param y Top or bottom edge of the view to add
08.
* @param flow If flow is true, align top edge to y. If false, align bottom
09.
* edge to y.
10.
* @param childrenLeft Left edge where children should be positioned
11.
* @param selected Is this position selected?
12.
* @return View that was added
13.
*/
14.
private
View makeAndAddView(
int
position,
int
y,
boolean
flow,
int
childrenLeft,
15.
boolean
selected) {
16.
View child;
17.
if
(!mDataChanged) {
18.
// Try to use an exsiting view for this position
19.
child = mRecycler.getActiveView(position);
20.
if
(child !=
null
) {
21.
// Found it -- we're using an existing child
22.
// This just needs to be positioned
23.
setupChild(child, position, y, flow, childrenLeft, selected,
true
);
24.
return
child;
25.
}
26.
}
27.
// Make a new view for this position, or convert an unused view if possible
28.
child = obtainView(position, mIsScrap);
29.
// This needs to be positioned and measured
30.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[
0
]);
31.
return
child;
32.
}
01.
/**
02.
* Get a view and have it show the data associated with the specified
03.
* position. This is called when we have already discovered that the view is
04.
* not available for reuse in the recycle bin. The only choices left are
05.
* converting an old view or making a new one.
06.
*
07.
* @param position
08.
* The position to display
09.
* @param isScrap
10.
* Array of at least 1 boolean, the first entry will become true
11.
* if the returned view was taken from the scrap heap, false if
12.
* otherwise.
13.
*
14.
* @return A view displaying the data associated with the specified position
15.
*/
16.
View obtainView(
int
position,
boolean
[] isScrap) {
17.
isScrap[
0
] =
false
;
18.
View scrapView;
19.
scrapView = mRecycler.getScrapView(position);
20.
View child;
21.
if
(scrapView !=
null
) {
22.
child = mAdapter.getView(position, scrapView,
this
);
23.
if
(child != scrapView) {
24.
mRecycler.addScrapView(scrapView);
25.
if
(mCacheColorHint !=
0
) {
26.
child.setDrawingCacheBackgroundColor(mCacheColorHint);
27.
}
28.
}
else
{
29.
isScrap[
0
] =
true
;
30.
dispatchFinishTemporaryDetach(child);
31.
}
32.
}
else
{
33.
child = mAdapter.getView(position,
null
,
this
);
34.
if
(mCacheColorHint !=
0
) {
35.
child.setDrawingCacheBackgroundColor(mCacheColorHint);
36.
}
37.
}
38.
return
child;
39.
}
那么我们平时写ListView的Adapter时,getView()方法通常会怎么写呢?这里我举个简单的例子:
01.
@Override
02.
public
View getView(
int
position, View convertView, ViewGroup parent) {
03.
Fruit fruit = getItem(position);
04.
View view;
05.
if
(convertView ==
null
) {
06.
view = LayoutInflater.from(getContext()).inflate(resourceId,
null
);
07.
}
else
{
08.
view = convertView;
09.
}
10.
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
11.
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
12.
fruitImage.setImageResource(fruit.getImageId());
13.
fruitName.setText(fruit.getName());
14.
return
view;
15.
}
那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:
01.
/**
02.
* Add a view as a child and make sure it is measured (if necessary) and
03.
* positioned properly.
04.
*
05.
* @param child The view to add
06.
* @param position The position of this child
07.
* @param y The y position relative to which this view will be positioned
08.
* @param flowDown If true, align top edge to y. If false, align bottom
09.
* edge to y.
10.
* @param childrenLeft Left edge where children should be positioned
11.
* @param selected Is this position selected?
12.
* @param recycled Has this view been pulled from the recycle bin? If so it
13.
* does not need to be remeasured.
14.
*/
15.
private
void
setupChild(View child,
int
position,
int
y,
boolean
flowDown,
int
childrenLeft,
16.
boolean
selected,
boolean
recycled) {
17.
final
boolean
isSelected = selected && shouldShowSelector();
18.
final
boolean
updateChildSelected = isSelected != child.isSelected();
19.
final
int
mode = mTouchMode;
20.
final
boolean
isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
21.
mMotionPosition == position;
22.
final
boolean
updateChildPressed = isPressed != child.isPressed();
23.
final
boolean
needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
24.
// Respect layout params that are already in the view. Otherwise make some up...
25.
// noinspection unchecked
26.
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
27.
if
(p ==
null
) {
28.
p =
new
AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
29.
ViewGroup.LayoutParams.WRAP_CONTENT,
0
);
30.
}
31.
p.viewType = mAdapter.getItemViewType(position);
32.
if
((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
33.
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
34.
attachViewToParent(child, flowDown ? -
1
:
0
, p);
35.
}
else
{
36.
p.forceAdd =
false
;
37.
if
(p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
38.
p.recycledHeaderFooter =
true
;
39.
}
40.
addViewInLayout(child, flowDown ? -
1
:
0
, p,
true
);
41.
}
42.
if
(updateChildSelected) {
43.
child.setSelected(isSelected);
44.
}
45.
if
(updateChildPressed) {
46.
child.setPressed(isPressed);
47.
}
48.
if
(needToMeasure) {
49.
int
childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
50.
mListPadding.left + mListPadding.right, p.width);
51.
int
lpHeight = p.height;
52.
int
childHeightSpec;
53.
if
(lpHeight >
0
) {
54.
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
55.
}
else
{
56.
childHeightSpec = MeasureSpec.makeMeasureSpec(
0
, MeasureSpec.UNSPECIFIED);
57.
}
58.
child.measure(childWidthSpec, childHeightSpec);
59.
}
else
{
60.
cleanupLayoutState(child);
61.
}
62.
final
int
w = child.getMeasuredWidth();
63.
final
int
h = child.getMeasuredHeight();
64.
final
int
childTop = flowDown ? y : y - h;
65.
if
(needToMeasure) {
66.
final
int
childRight = childrenLeft + w;
67.
final
int
childBottom = childTop + h;
68.
child.layout(childrenLeft, childTop, childRight, childBottom);
69.
}
else
{
70.
child.offsetLeftAndRight(childrenLeft - child.getLeft());
71.
child.offsetTopAndBottom(childTop - child.getTop());
72.
}
73.
if
(mCachingStarted && !child.isDrawingCacheEnabled()) {
74.
child.setDrawingCacheEnabled(
true
);
75.
}
76.
}
那么到此为止,第一次Layout过程结束。
第二次Layout
虽然我在源码中并没有找出具体的原因,但如果你自己做一下实验的话就会发现,即使是一个再简单的View,在展示到界面上之前都会经历至少两次onMeasure()和两次onLayout()的过程。其实这只是一个很小的细节,平时对我们影响并不大,因为不管是onMeasure()或者onLayout()几次,反正都是执行的相同的逻辑,我们并不需要进行过多关心。但是在ListView中情况就不一样了,因为这就意味着layoutChildren()过程会执行两次,而这个过程当中涉及到向ListView中添加子元素,如果相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。因此ListView在layoutChildren()过程当中做了第二次Layout的逻辑处理,非常巧妙地解决了这个问题,下面我们就来分析一下第二次Layout的过程。
其实第二次Layout和第一次Layout的基本流程是差不多的,那么我们还是从layoutChildren()方法开始看起:
001.
@Override
002.
protected
void
layoutChildren() {
003.
final
boolean
blockLayoutRequests = mBlockLayoutRequests;
004.
if
(!blockLayoutRequests) {
005.
mBlockLayoutRequests =
true
;
006.
}
else
{
007.
return
;
008.
}
009.
try
{
010.
super
.layoutChildren();
011.
invalidate();
012.
if
(mAdapter ==
null
) {
013.
resetList();
014.
invokeOnItemScrollListener();
015.
return
;
016.
}
017.
int
childrenTop = mListPadding.top;
018.
int
childrenBottom = getBottom() - getTop() - mListPadding.bottom;
019.
int
childCount = getChildCount();
020.
int
index =
0
;
021.
int
delta =
0
;
022.
View sel;
023.
View oldSel =
null
;
024.
View oldFirst =
null
;
025.
View newSel =
null
;
026.
View focusLayoutRestoreView =
null
;
027.
// Remember stuff we will need down below
028.
switch
(mLayoutMode) {
029.
case
LAYOUT_SET_SELECTION:
030.
index = mNextSelectedPosition - mFirstPosition;
031.
if
(index >=
0
&& index < childCount) {
032.
newSel = getChildAt(index);
033.
}
034.
break
;
035.
case
LAYOUT_FORCE_TOP:
036.
case
LAYOUT_FORCE_BOTTOM:
037.
case
LAYOUT_SPECIFIC:
038.
case
LAYOUT_SYNC:
039.
break
;
040.
case
LAYOUT_MOVE_SELECTION:
041.
default
:
042.
// Remember the previously selected view
043.
index = mSelectedPosition - mFirstPosition;
044.
if
(index >=
0
&& index < childCount) {
045.
oldSel = getChildAt(index);
046.
}
047.
// Remember the previous first child
048.
oldFirst = getChildAt(
0
);
049.
if
(mNextSelectedPosition >=
0
) {
050.
delta = mNextSelectedPosition - mSelectedPosition;
051.
}
052.
// Caution: newSel might be null
053.
newSel = getChildAt(index + delta);
054.
}
055.
boolean
dataChanged = mDataChanged;
056.
if
(dataChanged) {
057.
handleDataChanged();
058.
}
059.
// Handle the empty set by removing all views that are visible
060.
// and calling it a day
061.
if
(mItemCount ==
0
) {
062.
resetList();
063.
invokeOnItemScrollListener();
064.
return
;
065.
}
else
if
(mItemCount != mAdapter.getCount()) {
066.
throw
new
IllegalStateException(
"The content of the adapter has changed but "
067.
+
"ListView did not receive a notification. Make sure the content of "
068.
+
"your adapter is not modified from a background thread, but only "
069.
+
"from the UI thread. [in ListView("
+ getId() +
", "
+ getClass()
070.
+
") with Adapter("
+ mAdapter.getClass() +
")]"
);
071.
}
072.
setSelectedPositionInt(mNextSelectedPosition);
073.
// Pull all children into the RecycleBin.
074.
// These views will be reused if possible
075.
final
int
firstPosition = mFirstPosition;
076.
final
RecycleBin recycleBin = mRecycler;
077.
// reset the focus restoration
078.
View focusLayoutRestoreDirectChild =
null
;
079.
// Don't put header or footer views into the Recycler. Those are
080.
// already cached in mHeaderViews;
081.
if
(dataChanged) {
082.
for
(
int
i =
0
; i < childCount; i++) {
083.
recycleBin.addScrapView(getChildAt(i));
084.
if
(ViewDebug.TRACE_RECYCLER) {
085.
ViewDebug.trace(getChildAt(i),
086.
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
087.
}
088.
}
089.
}
else
{
090.
recycleBin.fillActiveViews(childCount, firstPosition);
091.
}
092.
// take focus back to us temporarily to avoid the eventual
093.
// call to clear focus when removing the focused child below
094.
// from messing things up when ViewRoot assigns focus back
095.
// to someone else
096.
final
View focusedChild = getFocusedChild();
097.
if
(focusedChild !=
null
) {
098.
// TODO: in some cases focusedChild.getParent() == null
099.
// we can remember the focused view to restore after relayout if the
100.
// data hasn't changed, or if the focused position is a header or footer
101.
if
(!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
102.
focusLayoutRestoreDirectChild = focusedChild;
103.
// remember the specific view that had focus
104.
focusLayoutRestoreView = findFocus();
105.
if
(focusLayoutRestoreView !=
null
) {
106.
// tell it we are going to mess with it
107.
focusLayoutRestoreView.onStartTemporaryDetach();
108.
}
109.
}
110.
requestFocus();
111.
}
112.
// Clear out old views
113.
detachAllViewsFromParent();
114.
switch
(mLayoutMode) {
115.
case
LAYOUT_SET_SELECTION:
116.
if
(newSel !=
null
) {
117.
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
118.
}
else
{
119.
sel = fillFromMiddle(childrenTop, childrenBottom);
120.
}
121.
break
;
122.
case
LAYOUT_SYNC:
123.
sel = fillSpecific(mSyncPosition, mSpecificTop);
124.
break
;
125.
case
LAYOUT_FORCE_BOTTOM:
126.
sel = fillUp(mItemCount -
1
, childrenBottom);
127.
adjustViewsUpOrDown();
128.
break
;
129.
case
LAYOUT_FORCE_TOP:
130.
mFirstPosition =
0
;
131.
sel = fillFromTop(childrenTop);
132.
adjustViewsUpOrDown();
133.
break
;
134.
case
LAYOUT_SPECIFIC:
135.
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
136.
break
;
137.
case
LAYOUT_MOVE_SELECTION:
138.
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
139.
break
;
140.
default
:
141.
if
(childCount ==
0
) {
142.
if
(!mStackFromBottom) {
143.
final
int
position = lookForSelectablePosition(
0
,
true
);
144.
setSelectedPositionInt(position);
145.
sel = fillFromTop(childrenTop);
146.
}
else
{
147.
final
int
position = lookForSelectablePosition(mItemCount -
1
,
false
);
148.
setSelectedPositionInt(position);
149.
sel = fillUp(mItemCount -
1
, childrenBottom);
150.
}
151.
}
else
{
152.
if
(mSelectedPosition >=
0
&& mSelectedPosition < mItemCount) {
153.
sel = fillSpecific(mSelectedPosition,
154.
oldSel ==
null
? childrenTop : oldSel.getTop());
155.
}
else
if
(mFirstPosition < mItemCount) {
156.
sel = fillSpecific(mFirstPosition,
157.
oldFirst ==
null
? childrenTop : oldFirst.getTop());
158.
}
else
{
159.
sel = fillSpecific(
0
, childrenTop);
160.
}
161.
}
162.
break
;
163.
}
164.
// Flush any cached views that did not get reused above
165.
recycleBin.scrapActiveViews();
166.
if
(sel !=
null
) {
167.
// the current selected item should get focus if items
168.
// are focusable
169.
if
(mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
170.
final
boolean
focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
171.
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
172.
if
(!focusWasTaken) {
173.
// selected item didn't take focus, fine, but still want
174.
// to make sure something else outside of the selected view
175.
// has focus
176.
final
View focused = getFocusedChild();
177.
if
(focused !=
null
) {
178.
focused.clearFocus();
179.
}
180.
positionSelector(sel);
181.
}
else
{
182.
sel.setSelected(
false
);
183.
mSelectorRect.setEmpty();
184.
}
185.
}
else
{
186.
positionSelector(sel);
187.
}
188.
mSelectedTop = sel.getTop();
189.
}
else
{
190.
if
(mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
191.
View child = getChildAt(mMotionPosition - mFirstPosition);
192.
if
(child !=
null
) positionSelector(child);
193.
}
else
{
194.
mSelectedTop =
0
;
195.
mSelectorRect.setEmpty();
196.
}
197.
// even if there is not selected position, we may need to restore
198.
// focus (i.e. something focusable in touch mode)
199.
if
(hasFocus() && focusLayoutRestoreView !=
null
) {
200.
focusLayoutRestoreView.requestFocus();
201.
}
202.
}
203.
// tell focus view we are done mucking with it, if it is still in
204.
// our view hierarchy.
205.
if
(focusLayoutRestoreView !=
null
206.
&& focusLayoutRestoreView.getWindowToken() !=
null
) {
207.
focusLayoutRestoreView.onFinishTemporaryDetach();
208.
}
209.
mLayoutMode = LAYOUT_NORMAL;
210.
mDataChanged =
false
;
211.
mNeedSync =
false
;
212.
setNextSelectedPositionInt(mSelectedPosition);
213.
updateScrollIndicators();
214.
if
(mItemCount >
0
) {
215.
checkSelectionChanged();
216.
}
217.
invokeOnItemScrollListener();
218.
}
finally
{
219.
if
(!blockLayoutRequests) {
220.
mBlockLayoutRequests =
false
;
221.
}
222.
}
223.
}
同样还是在第19行,调用getChildCount()方法来获取子View的数量,只不过现在得到的值不会再是0了,而是ListView中一屏可以显示的子View数量,因为我们刚刚在第一次Layout过程当中向ListView添加了这么多的子View。下面在第90行调用了RecycleBin的fillActiveViews()方法,这次效果可就不一样了,因为目前ListView中已经有子View了,这样所有的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。
接下来将会是非常非常重要的一个操作,在第113行调用了detachAllViewsFromParent()方法。这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。
那么我们接着看,在第141行的判断逻辑当中,由于不再等于0了,因此会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断通常是成立的,因为mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,代码如下所示:
01.
/**
02.
* Put a specific item at a specific location on the screen and then build
03.
* up and down from there.
04.
*
05.
* @param position The reference view to use as the starting point
06.
* @param top Pixel offset from the top of this view to the top of the
07.
* reference view.
08.
*
09.
* @return The selected view, or null if the selected view is outside the
10.
* visible area.
11.
*/
12.
private
View fillSpecific(
int
position,
int
top) {
13.
boolean
tempIsSelected = position == mSelectedPosition;
14.
View temp = makeAndAddView(position, top,
true
, mListPadding.left, tempIsSelected);
15.
// Possibly changed again in fillUp if we add rows above this one.
16.
mFirstPosition = position;
17.
View above;
18.
View below;
19.
final
int
dividerHeight = mDividerHeight;
20.
if
(!mStackFromBottom) {
21.
above = fillUp(position -
1
, temp.getTop() - dividerHeight);
22.
// This will correct for the top of the first view not touching the top of the list
23.
adjustViewsUpOrDown();
24.
below = fillDown(position +
1
, temp.getBottom() + dividerHeight);
25.
int
childCount = getChildCount();
26.
if
(childCount >
0
) {
27.
correctTooHigh(childCount);
28.
}
29.
}
else
{
30.
below = fillDown(position +
1
, temp.getBottom() + dividerHeight);
31.
// This will correct for the bottom of the last view not touching the bottom of the list
32.
adjustViewsUpOrDown();
33.
above = fillUp(position -
1
, temp.getTop() - dividerHeight);
34.
int
childCount = getChildCount();
35.
if
(childCount >
0
) {
36.
correctTooLow(childCount);
37.
}
38.
}
39.
if
(tempIsSelected) {
40.
return
temp;
41.
}
else
if
(above !=
null
) {
42.
return
above;
43.
}
else
{
44.
return
below;
45.
}
46.
}
fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:
01.
/**
02.
* Obtain the view and add it to our list of children. The view can be made
03.
* fresh, converted from an unused view, or used as is if it was in the
04.
* recycle bin.
05.
*
06.
* @param position Logical position in the list
07.
* @param y Top or bottom edge of the view to add
08.
* @param flow If flow is true, align top edge to y. If false, align bottom
09.
* edge to y.
10.
* @param childrenLeft Left edge where children should be positioned
11.
* @param selected Is this position selected?
12.
* @return View that was added
13.
*/
14.
private
View makeAndAddView(
int
position,
int
y,
boolean
flow,
int
childrenLeft,
15.
boolean
selected) {
16.
View child;
17.
if
(!mDataChanged) {
18.
// Try to use an exsiting view for this position
19.
child = mRecycler.getActiveView(position);
20.
if
(child !=
null
) {
21.
// Found it -- we're using an existing child
22.
// This just needs to be positioned
23.
setupChild(child, position, y, flow, childrenLeft, selected,
true
);
24.
return
child;
25.
}
26.
}
27.
// Make a new view for this position, or convert an unused view if possible
28.
child = obtainView(position, mIsScrap);
29.
// This needs to be positioned and measured
30.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[
0
]);
31.
return
child;
32.
}
仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。
注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:
01.
/**
02.
* Add a view as a child and make sure it is measured (if necessary) and
03.
* positioned properly.
04.
*
05.
* @param child The view to add
06.
* @param position The position of this child
07.
* @param y The y position relative to which this view will be positioned
08.
* @param flowDown If true, align top edge to y. If false, align bottom
09.
* edge to y.
10.
* @param childrenLeft Left edge where children should be positioned
11.
* @param selected Is this position selected?
12.
* @param recycled Has this view been pulled from the recycle bin? If so it
13.
* does not need to be remeasured.
14.
*/
15.
private
void
setupChild(View child,
int
position,
int
y,
boolean
flowDown,
int
childrenLeft,
16.
boolean
selected,
boolean
recycled) {
17.
final
boolean
isSelected = selected && shouldShowSelector();
18.
final
boolean
updateChildSelected = isSelected != child.isSelected();
19.
final
int
mode = mTouchMode;
20.
final
boolean
isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
21.
mMotionPosition == position;
22.
final
boolean
updateChildPressed = isPressed != child.isPressed();
23.
final
boolean
needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
24.
// Respect layout params that are already in the view. Otherwise make some up...
25.
// noinspection unchecked
26.
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
27.
if
(p ==
null
) {
28.
p =
new
AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
29.
ViewGroup.LayoutParams.WRAP_CONTENT,
0
);
30.
}
31.
p.viewType = mAdapter.getItemViewType(position);
32.
if
((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
33.
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
34.
attachViewToParent(child, flowDown ? -
1
:
0
, p);
35.
}
else
{
36.
p.forceAdd =
false
;
37.
if
(p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
38.
p.recycledHeaderFooter =
true
;
39.
}
40.
addViewInLayout(child, flowDown ? -
1
:
0
, p,
true
);
41.
}
42.
if
(updateChildSelected) {
43.
child.setSelected(isSelected);
44.
}
45.
if
(updateChildPressed) {
46.
child.setPressed(isPressed);
47.
}
48.
if
(needToMeasure) {
49.
int
childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
50.
mListPadding.left + mListPadding.right, p.width);
51.
int
lpHeight = p.height;
52.
int
childHeightSpec;
53.
if
(lpHeight >
0
) {
54.
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
55.
}
else
{
56.
childHeightSpec = MeasureSpec.makeMeasureSpec(
0
, MeasureSpec.UNSPECIFIED);
57.
}
58.
child.measure(childWidthSpec, childHeightSpec);
59.
}
else
{
60.
cleanupLayoutState(child);
61.
}
62.
final
int
w = child.getMeasuredWidth();
63.
final
int
h = child.getMeasuredHeight();
64.
final
int
childTop = flowDown ? y : y - h;
65.
if
(needToMeasure) {
66.
final
int
childRight = childrenLeft + w;
67.
final
int
childBottom = childTop + h;
68.
child.layout(childrenLeft, childTop, childRight, childBottom);
69.
}
else
{
70.
child.offsetLeftAndRight(childrenLeft - child.getLeft());
71.
child.offsetTopAndBottom(childTop - child.getTop());
72.
}
73.
if
(mCachingStarted && !child.isDrawingCacheEnabled()) {
74.
child.setDrawingCacheEnabled(
true
);
75.
}
76.
}
经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。
滑动加载更多数据
经历了两次Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。
由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码就肯定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:
001.
@Override
002.
public
boolean
onTouchEvent(MotionEvent ev) {
003.
if
(!isEnabled()) {
004.
// A disabled view that is clickable still consumes the touch
005.
// events, it just doesn't respond to them.
006.
return
isClickable() || isLongClickable();
007.
}
008.
final
int
action = ev.getAction();
009.
View v;
010.
int
deltaY;
011.
if
(mVelocityTracker ==
null
) {
012.
mVelocityTracker = VelocityTracker.obtain();
013.
}
014.
mVelocityTracker.addMovement(ev);
015.
switch
(action & MotionEvent.ACTION_MASK) {
016.
case
MotionEvent.ACTION_DOWN: {
017.
mActivePointerId = ev.getPointerId(
0
);
018.
final
int
x = (
int
) ev.getX();
019.
final
int
y = (
int
) ev.getY();
020.
int
motionPosition = pointToPosition(x, y);
021.
if
(!mDataChanged) {
022.
if
((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >=
0
)
023.
&& (getAdapter().isEnabled(motionPosition))) {
024.
// User clicked on an actual view (and was not stopping a
025.
// fling). It might be a
026.
// click or a scroll. Assume it is a click until proven
027.
// otherwise
028.
mTouchMode = TOUCH_MODE_DOWN;
029.
// FIXME Debounce
030.
if
(mPendingCheckForTap ==
null
) {
031.
mPendingCheckForTap =
new
CheckForTap();
032.
}
033.
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
034.
}
else
{
035.
if
(ev.getEdgeFlags() !=
0
&& motionPosition <
0
) {
036.
// If we couldn't find a view to click on, but the down
037.
// event was touching
038.
// the edge, we will bail out and try again. This allows
039.
// the edge correcting
040.
// code in ViewRoot to try to find a nearby view to
041.
// select
042.
return
false
;
043.
}
044.
045.
if
(mTouchMode == TOUCH_MODE_FLING) {
046.
// Stopped a fling. It is a scroll.
047.
createScrollingCache();
048.
mTouchMode = TOUCH_MODE_SCROLL;
049.
mMotionCorrection =
0
;
050.
motionPosition = findMotionRow(y);
051.
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
052.
}
053.
}
054.
}
055.
if
(motionPosition >=
0
) {
056.
// Remember where the motion event started
057.
v = getChildAt(motionPosition - mFirstPosition);
058.
mMotionViewOriginalTop = v.getTop();
059.
}
060.
mMotionX = x;
061.
mMotionY = y;
062.
mMotionPosition = motionPosition;
063.
mLastY = Integer.MIN_VALUE;
064.
break
;
065.
}
066.
case
MotionEvent.ACTION_MOVE: {
067.
final
int
pointerIndex = ev.findPointerIndex(mActivePointerId);
068.
final
int
y = (
int
) ev.getY(pointerIndex);
069.
deltaY = y - mMotionY;
070.
switch
(mTouchMode) {
071.
case
TOUCH_MODE_DOWN:
072.
case
TOUCH_MODE_TAP:
073.
case
TOUCH_MODE_DONE_WAITING:
074.
// Check if we have moved far enough that it looks more like a
075.
// scroll than a tap
076.
startScrollIfNeeded(deltaY);
077.
break
;
078.
case
TOUCH_MODE_SCROLL:
079.
if
(PROFILE_SCROLLING) {
080.
if
(!mScrollProfilingStarted) {
081.
Debug.startMethodTracing(
"AbsListViewScroll"
);
082.
mScrollProfilingStarted =
true
;
083.
}
084.
}
085.
if
(y != mLastY) {
086.
deltaY -= mMotionCorrection;
087.
int
incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
088.
// No need to do all this work if we're not going to move
089.
// anyway
090.
boolean
atEdge =
false
;
091.
if
(incrementalDeltaY !=
0
) {
092.
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
093.
}
094.
// Check to see if we have bumped into the scroll limit
095.
if
(atEdge && getChildCount() >
0
) {
096.
// Treat this like we're starting a new scroll from the
097.
// current
098.
// position. This will let the user start scrolling back
099.
// into
100.
// content immediately rather than needing to scroll
101.
// back to the
102.
// point where they hit the limit first.
103.
int
motionPosition = findMotionRow(y);
104.
if
(motionPosition >=
0
) {
105.
final
View motionView = getChildAt(motionPosition - mFirstPosition);
106.
mMotionViewOriginalTop = motionView.getTop();
107.
}
108.
mMotionY = y;
109.
mMotionPosition = motionPosition;
110.
invalidate();
111.
}
112.
mLastY = y;
113.
}
114.
break
;
115.
}
116.
break
;
117.
}
118.
case
MotionEvent.ACTION_UP: {
119.
switch
(mTouchMode) {
120.
case
TOUCH_MODE_DOWN:
121.
case
TOUCH_MODE_TAP:
122.
case
TOUCH_MODE_DONE_WAITING:
123.
final
int
motionPosition = mMotionPosition;
124.
final
View child = getChildAt(motionPosition - mFirstPosition);
125.
if
(child !=
null
&& !child.hasFocusable()) {
126.
if
(mTouchMode != TOUCH_MODE_DOWN) {
127.
child.setPressed(
false
);
128.
}
129.
if
(mPerformClick ==
null
) {
130.
mPerformClick =
new
PerformClick();
131.
}
132.
final
AbsListView.PerformClick performClick = mPerformClick;
133.
performClick.mChild = child;
134.
performClick.mClickMotionPosition = motionPosition;
135.
performClick.rememberWindowAttachCount();
136.
mResurrectToPosition = motionPosition;
137.
if
(mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
138.
final
Handler handler = getHandler();
139.
if
(handler !=
null
) {
140.
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
141.
: mPendingCheckForLongPress);
142.
}
143.
mLayoutMode = LAYOUT_NORMAL;
144.
if
(!mDataChanged && mAdapter.isEnabled(motionPosition)) {
145.
mTouchMode = TOUCH_MODE_TAP;
146.
setSelectedPositionInt(mMotionPosition);
147.
layoutChildren();
148.
child.setPressed(
true
);
149.
positionSelector(child);
150.
setPressed(
true
);
151.
if
(mSelector !=
null
) {
152.
Drawable d = mSelector.getCurrent();
153.
if
(d !=
null
&& d
instanceof
TransitionDrawable) {
154.
((TransitionDrawable) d).resetTransition();
155.
}
156.
}
157.
postDelayed(
new
Runnable() {
158.
public
void
run() {
159.
child.setPressed(
false
);
160.
setPressed(
false
);
161.
if
(!mDataChanged) {
162.
post(performClick);
163.
}
164.
mTouchMode = TOUCH_MODE_REST;
165.
}
166.
}, ViewConfiguration.getPressedStateDuration());
167.
}
else
{
168.
mTouchMode = TOUCH_MODE_REST;
169.
}
170.
return
true
;
171.
}
else
if
(!mDataChanged && mAdapter.isEnabled(motionPosition)) {
172.
post(performClick);
173.
}
174.
}
175.
mTouchMode = TOUCH_MODE_REST;
176.
break
;
177.
case
TOUCH_MODE_SCROLL:
178.
final
int
childCount = getChildCount();
179.
if
(childCount >
0
) {
180.
if
(mFirstPosition ==
0
181.
&& getChildAt(
0
).getTop() >= mListPadding.top
182.
&& mFirstPosition + childCount < mItemCount
183.
&& getChildAt(childCount -
1
).getBottom() <= getHeight()
184.
- mListPadding.bottom) {
185.
mTouchMode = TOUCH_MODE_REST;
186.
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
187.
}
else
{
188.
final
VelocityTracker velocityTracker = mVelocityTracker;
189.
velocityTracker.computeCurrentVelocity(
1000
, mMaximumVelocity);
190.
final
int
initialVelocity = (
int
) velocityTracker
191.
.getYVelocity(mActivePointerId);
192.
if
(Math.abs(initialVelocity) > mMinimumVelocity) {
193.
if
(mFlingRunnable ==
null
) {
194.
mFlingRunnable =
new
FlingRunnable();
195.
}
196.
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
197.
mFlingRunnable.start(-initialVelocity);
198.
}
else
{
199.
mTouchMode = TOUCH_MODE_REST;
200.
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
201.
}
202.
}
203.
}
else
{
204.
mTouchMode = TOUCH_MODE_REST;
205.
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
206.
}
207.
break
;
208.
}
209.
setPressed(
false
);
210.
// Need to redraw since we probably aren't drawing the selector
211.
// anymore
212.
invalidate();
213.
final
Handler handler = getHandler();
214.
if
(handler !=
null
) {
215.
handler.removeCallbacks(mPendingCheckForLongPress);
216.
}
217.
if
(mVelocityTracker !=
null
) {
218.
mVelocityTracker.recycle();
219.
mVelocityTracker =
null
;
220.
}
221.
mActivePointerId = INVALID_POINTER;
222.
if
(PROFILE_SCROLLING) {
223.
if
(mScrollProfilingStarted) {
224.
Debug.stopMethodTracing();
225.
mScrollProfilingStarted =
false
;
226.
}
227.
}
228.
break
;
229.
}
230.
case
MotionEvent.ACTION_CANCEL: {
231.
mTouchMode = TOUCH_MODE_REST;
232.
setPressed(
false
);
233.
View motionView =
this
.getChildAt(mMotionPosition - mFirstPosition);
234.
if
(motionView !=
null
) {
235.
motionView.setPressed(
false
);
236.
}
237.
clearScrollingCache();
238.
final
Handler handler = getHandler();
239.
if
(handler !=
null
) {
240.
handler.removeCallbacks(mPendingCheckForLongPress);
241.
}
242.
if
(mVelocityTracker !=
null
) {
243.
mVelocityTracker.recycle();
244.
mVelocityTracker =
null
;
245.
}
246.
mActivePointerId = INVALID_POINTER;
247.
break
;
248.
}
249.
case
MotionEvent.ACTION_POINTER_UP: {
250.
onSecondaryPointerUp(ev);
251.
final
int
x = mMotionX;
252.
final
int
y = mMotionY;
253.
final
int
motionPosition = pointToPosition(x, y);
254.
if
(motionPosition >=
0
) {
255.
// Remember where the motion event started
256.
v = getChildAt(motionPosition - mFirstPosition);
257.
mMotionViewOriginalTop = v.getTop();
258.
mMotionPosition = motionPosition;
259.
}
260.
mLastY = y;
261.
break
;
262.
}
263.
}
264.
return
true
;
265.
}
这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,那么我们就只看这部分代码就可以了。
可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解了,喜欢寻根究底的朋友们可以自己去源码里找一找原因。
这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并没有什么太多需要注意的东西,唯一一点非常重要的就是第92行调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。那么我们进入到这个方法中瞧一瞧,代码如下所示:
01.
boolean
trackMotionScroll(
int
deltaY,
int
incrementalDeltaY) {
02.
final
int
childCount = getChildCount();
03.
if
(childCount ==
0
) {
04.
return
true
;
05.
}
06.
final
int
firstTop = getChildAt(
0
).getTop();
07.
final
int
lastBottom = getChildAt(childCount -
1
).getBottom();
08.
final
Rect listPadding = mListPadding;
09.
final
int
spaceAbove = listPadding.top - firstTop;
10.
final
int
end = getHeight() - listPadding.bottom;
11.
final
int
spaceBelow = lastBottom - end;
12.
final
int
height = getHeight() - getPaddingBottom() - getPaddingTop();
13.
if
(deltaY <
0
) {
14.
deltaY = Math.max(-(height -
1
), deltaY);
15.
}
else
{
16.
deltaY = Math.min(height -
1
, deltaY);
17.
}
18.
if
(incrementalDeltaY <
0
) {
19.
incrementalDeltaY = Math.max(-(height -
1
), incrementalDeltaY);
20.
}
else
{
21.
incrementalDeltaY = Math.min(height -
1
, incrementalDeltaY);
22.
}
23.
final
int
firstPosition = mFirstPosition;
24.
if
(firstPosition ==
0
&& firstTop >= listPadding.top && deltaY >=
0
) {
25.
// Don't need to move views down if the top of the first position
26.
// is already visible
27.
return
true
;
28.
}
29.
if
(firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <=
0
) {
30.
// Don't need to move views up if the bottom of the last position
31.
// is already visible
32.
return
true
;
33.
}
34.
final
boolean
down = incrementalDeltaY <
0
;
35.
final
boolean
inTouchMode = isInTouchMode();
36.
if
(inTouchMode) {
37.
hideSelector();
38.
}
39.
final
int
headerViewsCount = getHeaderViewsCount();
40.
final
int
footerViewsStart = mItemCount - getFooterViewsCount();
41.
int
start =
0
;
42.
int
count =
0
;
43.
if
(down) {
44.
final
int
top = listPadding.top - incrementalDeltaY;
45.
for
(
int
i =
0
; i < childCount; i++) {
46.
fina