轉載請注明出處:[http://blog.csdn.net/guolin_blog/article/details/44996879](http://blog.csdn.net/guolin_blog/article/details/44996879)
在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中的主要代碼,如下所示:
~~~
/**
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin
* has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
* those views which were onscreen at the start of a layout. By
* construction, they are displaying current information. At the end of
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
* are old views that could potentially be used by the adapter to avoid
* allocating views unnecessarily.
*
* @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
* @see android.widget.AbsListView.RecyclerListener
*/
class RecycleBin {
private RecyclerListener mRecyclerListener;
/**
* The position of the first view stored in mActiveViews.
*/
private int mFirstActivePosition;
/**
* Views that were on screen at the start of layout. This array is
* populated at the start of layout, and at the end of layout all view
* in mActiveViews are moved to mScrapViews. Views in mActiveViews
* represent a contiguous range of Views, with position of the first
* view store in mFirstActivePosition.
*/
private View[] mActiveViews = new View[0];
/**
* Unsorted views that can be used by the adapter as a convert view.
*/
private ArrayList<View>[] mScrapViews;
private int mViewTypeCount;
private ArrayList<View> mCurrentScrap;
/**
* Fill ActiveViews with all of the children of the AbsListView.
*
* @param childCount
* The minimum number of views mActiveViews should hold
* @param firstActivePosition
* The position of the first view that will be stored in
* mActiveViews
*/
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();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
// active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
}
}
}
/**
* Get the view corresponding to the specified position. The view will
* be removed from mActiveViews if it is found.
*
* @param position
* The position to look up in mActiveViews
* @return The view if it is found, null otherwise
*/
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;
}
/**
* Put a view into the ScapViews list. These views are unordered.
*
* @param scrap
* The view to add
*/
void addScrapView(View scrap) {
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
// Don't put header or footer views or views that should be ignored
// into the scrap heap
int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(scrap, false);
}
return;
}
if (mViewTypeCount == 1) {
dispatchFinishTemporaryDetach(scrap);
mCurrentScrap.add(scrap);
} else {
dispatchFinishTemporaryDetach(scrap);
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
ArrayList<View> scrapViews;
if (mViewTypeCount == 1) {
scrapViews = mCurrentScrap;
int size = scrapViews.size();
if (size > 0) {
return scrapViews.remove(size - 1);
} else {
return null;
}
} else {
int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
scrapViews = mScrapViews[whichScrap];
int size = scrapViews.size();
if (size > 0) {
return scrapViews.remove(size - 1);
}
}
}
return null;
}
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
// noinspection unchecked
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;
}
}
~~~
這里的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(二)**](http://blog.csdn.net/guolin_blog/article/details/16330267)?。
View的執行流程無非就分為三步,onMeasure()用于測量View的大小,onLayout()用于確定View的布局,onDraw()用于將View繪制到界面上。而在ListView當中,onMeasure()并沒有什么特殊的地方,因為它終歸是一個View,占用的空間最多并且通常也就是整個屏幕。onDraw()在ListView當中也沒有什么意義,因為ListView本身并不負責繪制,而是由ListView當中的子元素來進行繪制的。那么ListView大部分的神奇功能其實都是在onLayout()方法中進行的了,因此我們本篇文章也是主要分析的這個方法里的內容。
如果你到ListView源碼中去找一找,你會發現ListView中是沒有onLayout()這個方法的,這是因為這個方法是在ListView的父類AbsListView中實現的,代碼如下所示:
~~~
/**
* Subclasses should NOT override this method but {@link #layoutChildren()}
* instead.
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
}
~~~
可以看到,onLayout()方法中并沒有做什么復雜的邏輯操作,主要就是一個判斷,如果ListView的大小或者位置發生了變化,那么changed變量就會變成true,此時會要求所有的子布局都強制進行重繪。除此之外倒沒有什么難理解的地方了,不過我們注意到,在第16行調用了layoutChildren()這個方法,從方法名上我們就可以猜出這個方法是用來進行子元素布局的,不過進入到這個方法當中你會發現這是個空方法,沒有一行代碼。這當然是可以理解的了,因為子元素的布局應該是由具體的實現類來負責完成的,而不是由父類完成。那么進入ListView的layoutChildren()方法,代碼如下所示:
~~~
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (!blockLayoutRequests) {
mBlockLayoutRequests = true;
} else {
return;
}
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
int childrenTop = mListPadding.top;
int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
View focusLayoutRestoreView = null;
// Remember stuff we will need down below
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:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// Handle the empty set by removing all views that are visible
// and calling it a day
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. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
setSelectedPositionInt(mNextSelectedPosition);
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
// reset the focus restoration
View focusLayoutRestoreDirectChild = null;
// Don't put header or footer views into the Recycler. Those are
// already cached in mHeaderViews;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i));
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(getChildAt(i),
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
}
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// take focus back to us temporarily to avoid the eventual
// call to clear focus when removing the focused child below
// from messing things up when ViewRoot assigns focus back
// to someone else
final View focusedChild = getFocusedChild();
if (focusedChild != null) {
// TODO: in some cases focusedChild.getParent() == null
// we can remember the focused view to restore after relayout if the
// data hasn't changed, or if the focused position is a header or footer
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
focusLayoutRestoreDirectChild = focusedChild;
// remember the specific view that had focus
focusLayoutRestoreView = findFocus();
if (focusLayoutRestoreView != null) {
// tell it we are going to mess with it
focusLayoutRestoreView.onStartTemporaryDetach();
}
}
requestFocus();
}
// Clear out old views
detachAllViewsFromParent();
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:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
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;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
if (sel != null) {
// the current selected item should get focus if items
// are focusable
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
if (!focusWasTaken) {
// selected item didn't take focus, fine, but still want
// to make sure something else outside of the selected view
// has focus
final View focused = getFocusedChild();
if (focused != null) {
focused.clearFocus();
}
positionSelector(sel);
} else {
sel.setSelected(false);
mSelectorRect.setEmpty();
}
} else {
positionSelector(sel);
}
mSelectedTop = sel.getTop();
} else {
if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
View child = getChildAt(mMotionPosition - mFirstPosition);
if (child != null) positionSelector(child);
} else {
mSelectedTop = 0;
mSelectorRect.setEmpty();
}
// even if there is not selected position, we may need to restore
// focus (i.e. something focusable in touch mode)
if (hasFocus() && focusLayoutRestoreView != null) {
focusLayoutRestoreView.requestFocus();
}
}
// tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.onFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
~~~
這段代碼比較長,我們挑重點的看。首先可以確定的是,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()方法,我們跟進去瞧一瞧:
~~~
/**
* Fills the list from top to bottom, starting with mFirstPosition
*
* @param nextTop The location where the top of the first item should be
* drawn
*
* @return The view that is currently selected
*/
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()方法中完成的。進入fillDown()方法,代碼如下所示:
~~~
/**
* Fills the list from pos down to the end of the list view.
*
* @param pos The first position to put in the list
*
* @param nextTop The location where the top of the item associated with pos
* should be drawn
*
* @return The view that is currently selected, if it happens to be in the
* range that we draw.
*/
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (getBottom() - getTop()) - mListPadding.bottom;
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
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循環當中又做了什么事情呢?值得讓人留意的就是第18行調用的makeAndAddView()方法,進入到這個方法當中,代碼如下所示:
~~~
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
~~~
這里在第19行嘗試從RecycleBin當中快速獲取一個active view,不過很遺憾的是目前RecycleBin當中還沒有緩存任何的View,所以這里得到的值肯定是null。那么取得了null之后就會繼續向下運行,到第28行會調用obtainView()方法來再次嘗試獲取一個View,這次的obtainView()方法是可以保證一定返回一個View的,于是下面立刻將獲取到的View傳入到了setupChild()方法當中。那么obtainView()內部到底是怎么工作的呢?我們先進入到這個方法里面看一下:
~~~
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
~~~
obtainView()方法中的代碼并不多,但卻包含了非常非常重要的邏輯,不夸張的說,整個ListView中最重要的內容可能就在這個方法里了。那么我們還是按照執行流程來看,在第19行代碼中調用了RecycleBin的getScrapView()方法來嘗試獲取一個廢棄緩存中的View,同樣的道理,這里肯定是獲取不到的,getScrapView()方法會返回一個null。這時該怎么辦呢?沒有關系,代碼會執行到第33行,調用mAdapter的getView()方法來去獲取一個View。那么mAdapter是什么呢?當然就是當前ListView關聯的適配器了。而getView()方法又是什么呢?還用說嗎,這個就是我們平時使用ListView時最最經常重寫的一個方法了,這里getView()方法中傳入了三個參數,分別是position,null和this。
那么我們平時寫ListView的Adapter時,getView()方法通常會怎么寫呢?這里我舉個簡單的例子:
~~~
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
~~~
getView()方法接受的三個參數,第一個參數position代表當前子元素的的位置,我們可以通過具體的位置來獲取與其相關的數據。第二個參數convertView,剛才傳入的是null,說明沒有convertView可以利用,因此我們會調用LayoutInflater的inflate()方法來去加載一個布局。接下來會對這個view進行一些屬性和值的設定,最后將view返回。
那么這個View也會作為obtainView()的結果進行返回,并最終傳入到setupChild()方法當中。其實也就是說,第一次layout過程當中,所有的子View都是調用LayoutInflater的inflate()方法加載出來的,這樣就會相對比較耗時,但是不用擔心,后面就不會再有這種情況了,那么我們繼續往下看:
~~~
/**
* Add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child The view to add
* @param position The position of this child
* @param y The y position relative to which this view will be positioned
* @param flowDown If true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @param recycled Has this view been pulled from the recycle bin? If so it
* does not need to be remeasured.
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
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 = !recycled || updateChildSelected || child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
p.viewType = mAdapter.getItemViewType(position);
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (needToMeasure) {
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
}
}
~~~
setupChild()方法當中的代碼雖然比較多,但是我們只看核心代碼的話就非常簡單了,剛才調用obtainView()方法獲取到的子元素View,這里在第40行調用了addViewInLayout()方法將它添加到了ListView當中。那么根據fillDown()方法中的while循環,會讓子元素View將整個ListView控件填滿然后就跳出,也就是說即使我們的Adapter中有一千條數據,ListView也只會加載第一屏的數據,剩下的數據反正目前在屏幕上也看不到,所以不會去做多余的加載工作,這樣就可以保證ListView中的內容能夠迅速展示到屏幕上。
那么到此為止,第一次Layout過程結束。
### 第二次Layout
雖然我在源碼中并沒有找出具體的原因,但如果你自己做一下實驗的話就會發現,即使是一個再簡單的View,在展示到界面上之前都會經歷至少兩次onMeasure()和兩次onLayout()的過程。其實這只是一個很小的細節,平時對我們影響并不大,因為不管是onMeasure()或者onLayout()幾次,反正都是執行的相同的邏輯,我們并不需要進行過多關心。但是在ListView中情況就不一樣了,因為這就意味著layoutChildren()過程會執行兩次,而這個過程當中涉及到向ListView中添加子元素,如果相同的邏輯執行兩遍的話,那么ListView中就會存在一份重復的數據了。因此ListView在layoutChildren()過程當中做了第二次Layout的邏輯處理,非常巧妙地解決了這個問題,下面我們就來分析一下第二次Layout的過程。
其實第二次Layout和第一次Layout的基本流程是差不多的,那么我們還是從layoutChildren()方法開始看起:
~~~
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (!blockLayoutRequests) {
mBlockLayoutRequests = true;
} else {
return;
}
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
int childrenTop = mListPadding.top;
int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
View focusLayoutRestoreView = null;
// Remember stuff we will need down below
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:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// Handle the empty set by removing all views that are visible
// and calling it a day
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. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
setSelectedPositionInt(mNextSelectedPosition);
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
// reset the focus restoration
View focusLayoutRestoreDirectChild = null;
// Don't put header or footer views into the Recycler. Those are
// already cached in mHeaderViews;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i));
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(getChildAt(i),
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
}
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// take focus back to us temporarily to avoid the eventual
// call to clear focus when removing the focused child below
// from messing things up when ViewRoot assigns focus back
// to someone else
final View focusedChild = getFocusedChild();
if (focusedChild != null) {
// TODO: in some cases focusedChild.getParent() == null
// we can remember the focused view to restore after relayout if the
// data hasn't changed, or if the focused position is a header or footer
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
focusLayoutRestoreDirectChild = focusedChild;
// remember the specific view that had focus
focusLayoutRestoreView = findFocus();
if (focusLayoutRestoreView != null) {
// tell it we are going to mess with it
focusLayoutRestoreView.onStartTemporaryDetach();
}
}
requestFocus();
}
// Clear out old views
detachAllViewsFromParent();
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:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
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;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
if (sel != null) {
// the current selected item should get focus if items
// are focusable
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
if (!focusWasTaken) {
// selected item didn't take focus, fine, but still want
// to make sure something else outside of the selected view
// has focus
final View focused = getFocusedChild();
if (focused != null) {
focused.clearFocus();
}
positionSelector(sel);
} else {
sel.setSelected(false);
mSelectorRect.setEmpty();
}
} else {
positionSelector(sel);
}
mSelectedTop = sel.getTop();
} else {
if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
View child = getChildAt(mMotionPosition - mFirstPosition);
if (child != null) positionSelector(child);
} else {
mSelectedTop = 0;
mSelectorRect.setEmpty();
}
// even if there is not selected position, we may need to restore
// focus (i.e. something focusable in touch mode)
if (hasFocus() && focusLayoutRestoreView != null) {
focusLayoutRestoreView.requestFocus();
}
}
// tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.onFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
~~~
同樣還是在第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()方法當中,代碼如下所示:
~~~
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) {
above = fillUp(position - 1, temp.getTop() - dividerHeight);
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown();
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
~~~
fillSpecific()這算是一個新方法了,不過其實它和fillUp()、fillDown()方法功能也是差不多的,主要的區別在于,fillSpecific()方法會優先將指定位置的子View先加載到屏幕上,然后再加載該子View往上以及往下的其它子View。那么由于這里我們傳入的position就是第一個子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,這里我們就不去關注太多它的細節,而是將精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代碼如下所示:
~~~
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
~~~
仍然還是在第19行嘗試從RecycleBin當中獲取Active View,然而這次就一定可以獲取到了,因為前面我們調用了RecycleBin的fillActiveViews()方法來緩存子View。那么既然如此,就不會再進入到第28行的obtainView()方法,而是會直接進入setupChild()方法當中,這樣也省去了很多時間,因為如果在obtainView()方法中又要去infalte布局的話,那么ListView的初始加載效率就大大降低了。
注意在第23行,setupChild()方法的最后一個參數傳入的是true,這個參數表明當前的View是之前被回收過的,那么我們再次回到setupChild()方法當中:
~~~
/**
* Add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child The view to add
* @param position The position of this child
* @param y The y position relative to which this view will be positioned
* @param flowDown If true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @param recycled Has this view been pulled from the recycle bin? If so it
* does not need to be remeasured.
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
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 = !recycled || updateChildSelected || child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
p.viewType = mAdapter.getItemViewType(position);
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (needToMeasure) {
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, 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);
}
}
~~~
可以看到,setupChild()方法的最后一個參數是recycled,然后在第32行會對這個變量進行判斷,由于recycled現在是true,所以會執行attachViewToParent()方法,而第一次Layout過程則是執行的else語句中的addViewInLayout()方法。這兩個方法最大的區別在于,如果我們需要向ViewGroup中添加一個新的子View,應該調用addViewInLayout()方法,而如果是想要將一個之前detach的View重新attach到ViewGroup上,就應該調用attachViewToParent()方法。那么由于前面在layoutChildren()方法當中調用了detachAllViewsFromParent()方法,這樣ListView中所有的子View都是處于detach狀態的,所以這里attachViewToParent()方法是正確的選擇。
經歷了這樣一個detach又attach的過程,ListView中所有的子View又都可以正常顯示出來了,那么第二次Layout過程結束。
### 滑動加載更多數據
經歷了兩次Layout過程,雖說我們已經可以在ListView中看到內容了,然而關于ListView最神奇的部分我們卻還沒有接觸到,因為目前ListView中只是加載并顯示了第一屏的數據而已。比如說我們的Adapter當中有1000條數據,但是第一屏只顯示了10條,ListView中也只有10個子View而已,那么剩下的990是怎樣工作并顯示到界面上的呢?這就要看一下ListView滑動部分的源碼了,因為我們是通過手指滑動來顯示更多數據的。
由于滑動部分的機制是屬于通用型的,即ListView和GridView都會使用同樣的機制,因此這部分代碼就肯定是寫在AbsListView當中的了。那么監聽觸控事件是在onTouchEvent()方法當中進行的,我們就來看一下AbsListView中的這個方法:
~~~
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
final int action = ev.getAction();
View v;
int deltaY;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
&& (getAdapter().isEnabled(motionPosition))) {
// User clicked on an actual view (and was not stopping a
// fling). It might be a
// click or a scroll. Assume it is a click until proven
// otherwise
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
// If we couldn't find a view to click on, but the down
// event was touching
// the edge, we will bail out and try again. This allows
// the edge correcting
// code in ViewRoot to try to find a nearby view to
// select
return false;
}
if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRow(y);
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final int y = (int) ev.getY(pointerIndex);
deltaY = y - mMotionY;
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
// Check if we have moved far enough that it looks more like a
// scroll than a tap
startScrollIfNeeded(deltaY);
break;
case TOUCH_MODE_SCROLL:
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("AbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (y != mLastY) {
deltaY -= mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
// No need to do all this work if we're not going to move
// anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
if (atEdge && getChildCount() > 0) {
// Treat this like we're starting a new scroll from the
// current
// position. This will let the user start scrolling back
// into
// content immediately rather than needing to scroll
// back to the
// point where they hit the limit first.
int motionPosition = findMotionRow(y);
if (motionPosition >= 0) {
final View motionView = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = motionView.getTop();
}
mMotionY = y;
mMotionPosition = motionPosition;
invalidate();
}
mLastY = y;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mChild = child;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
mResurrectToPosition = motionPosition;
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
: mPendingCheckForLongPress);
}
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(child);
setPressed(true);
if (mSelector != null) {
Drawable d = mSelector.getCurrent();
if (d != null && d instanceof TransitionDrawable) {
((TransitionDrawable) d).resetTransition();
}
}
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_REST;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
}
return true;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
mTouchMode = TOUCH_MODE_REST;
break;
case TOUCH_MODE_SCROLL:
final int childCount = getChildCount();
if (childCount > 0) {
if (mFirstPosition == 0
&& getChildAt(0).getTop() >= mListPadding.top
&& mFirstPosition + childCount < mItemCount
&& getChildAt(childCount - 1).getBottom() <= getHeight()
- mListPadding.bottom) {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final int initialVelocity = (int) velocityTracker
.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinimumVelocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new FlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
break;
}
setPressed(false);
// Need to redraw since we probably aren't drawing the selector
// anymore
invalidate();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
if (PROFILE_SCROLLING) {
if (mScrollProfilingStarted) {
Debug.stopMethodTracing();
mScrollProfilingStarted = false;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);
View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
clearScrollingCache();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
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) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
return true;
}
~~~
這個方法中的代碼就非常多了,因為它所處理的邏輯也非常多,要監聽各種各樣的觸屏事件。但是我們目前所關心的就只有手指在屏幕上滑動這一個事件而已,對應的是ACTION_MOVE這個動作,那么我們就只看這部分代碼就可以了。
可以看到,ACTION_MOVE這個case里面又嵌套了一個switch語句,是根據當前的TouchMode來選擇的。那這里我可以直接告訴大家,當手指在屏幕上滑動時,TouchMode是等于TOUCH_MODE_SCROLL這個值的,至于為什么那又要牽扯到另外的好幾個方法,這里限于篇幅原因就不再展開講解了,喜歡尋根究底的朋友們可以自己去源碼里找一找原因。
這樣的話,代碼就應該會走到第78行的這個case里面去了,在這個case當中并沒有什么太多需要注意的東西,唯一一點非常重要的就是第92行調用的trackMotionScroll()方法,相當于我們手指只要在屏幕上稍微有一點點移動,這個方法就會被調用,而如果是正常在屏幕上滑動的話,那么這個方法就會被調用很多次。那么我們進入到這個方法中瞧一瞧,代碼如下所示:
~~~
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;
final int spaceAbove = listPadding.top - firstTop;
final int end = getHeight() - listPadding.bottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - getPaddingBottom() - getPaddingTop();
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 && firstTop >= listPadding.top && deltaY >= 0) {
// Don't need to move views down if the top of the first position
// is already visible
return true;
}
if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
// Don't need to move views up if the bottom of the last position
// is already visible
return true;
}
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) {
final int top = listPadding.top - incrementalDeltaY;
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) {
mRecycler.addScrapView(child);
}
}
}
} else {
final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
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) {
mRecycler.addScrapView(child);
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
invalidate();
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(getChildAt(childIndex));
}
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
awakenScrollBars();
return false;
}
~~~
這個方法接收兩個參數,deltaY表示從手指按下時的位置到當前手指位置的距離,incrementalDeltaY則表示據上次觸發event事件手指在Y方向上位置的改變量,那么其實我們就可以通過incrementalDeltaY的正負值情況來判斷用戶是向上還是向下滑動的了。如第34行代碼所示,如果incrementalDeltaY小于0,說明是向下滑動,否則就是向上滑動。
下面將會進行一個邊界值檢測的過程,可以看到,從第43行開始,當ListView向下滑動的時候,就會進入一個for循環當中,從上往下依次獲取子View,第47行當中,如果該子View的bottom值已經小于top值了,就說明這個子View已經移出屏幕了,所以會調用RecycleBin的addScrapView()方法將這個View加入到廢棄緩存當中,并將count計數器加1,計數器用于記錄有多少個子View被移出了屏幕。那么如果是ListView向上滑動的話,其實過程是基本相同的,只不過變成了從下往上依次獲取子View,然后判斷該子View的top值是不是大于bottom值了,如果大于的話說明子View已經移出了屏幕,同樣把它加入到廢棄緩存中,并將計數器加1。
接下來在第76行,會根據當前計數器的值來進行一個detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念當中,所有看不到的View就沒有必要為它進行保存,因為屏幕外還有成百上千條數據等著顯示呢,一個好的回收策略才能保證ListView的高性能和高效率。緊接著在第78行調用了offsetChildrenTopAndBottom()方法,并將incrementalDeltaY作為參數傳入,這個方法的作用是讓ListView中所有的子View都按照傳入的參數值進行相應的偏移,這樣就實現了隨著手指的拖動,ListView的內容也會隨著滾動的效果。
然后在第84行會進行判斷,如果ListView中最后一個View的底部已經移入了屏幕,或者ListView中第一個View的頂部移入了屏幕,就會調用fillGap()方法,那么因此我們就可以猜出fillGap()方法是用來加載屏幕外數據的,進入到這個方法中瞧一瞧,如下所示:
~~~
/**
* Fills the gap left open by a touch-scroll. During a touch scroll,
* children that remain on screen are shifted and the other ones are
* discarded. The role of this method is to fill the gap thus created by
* performing a partial layout in the empty space.
*
* @param down
* true if the scroll is going down, false if it is going up
*/
abstract void fillGap(boolean down);
~~~
down參數用于表示ListView是向下滑動還是向上滑動的,可以看到,如果是向下滑動的話就會調用fillDown()方法,而如果是向上滑動的話就會調用fillUp()方法。那么這兩個方法我們都已經非常熟悉了,內部都是通過一個循環來去對ListView進行填充,所以這兩個方法我們就不看了,但是填充ListView會通過調用makeAndAddView()方法來完成,又是makeAndAddView()方法,但這次的邏輯再次不同了,所以我們還是回到這個方法瞧一瞧:
~~~
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
getListPaddingTop();
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - getListPaddingBottom();
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
~~~
不管怎么說,這里首先仍然是會嘗試調用RecycleBin的getActiveView()方法來獲取子布局,只不過肯定是獲取不到的了,因為在第二次Layout過程中我們已經從mActiveViews中獲取過了數據,而根據RecycleBin的機制,mActiveViews是不能夠重復利用的,因此這里返回的值肯定是null。
既然getActiveView()方法返回的值是null,那么就還是會走到第28行的obtainView()方法當中,代碼如下所示:
~~~
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
~~~
這里在第19行會調用RecyleBin的getScrapView()方法來嘗試從廢棄緩存中獲取一個View,那么廢棄緩存有沒有View呢?當然有,因為剛才在trackMotionScroll()方法中我們就已經看到了,一旦有任何子View被移出了屏幕,就會將它加入到廢棄緩存中,而從obtainView()方法中的邏輯來看,一旦有新的數據需要顯示到屏幕上,就會嘗試從廢棄緩存中獲取View。所以它們之間就形成了一個生產者和消費者的模式,那么ListView神奇的地方也就在這里體現出來了,不管你有任意多條數據需要顯示,ListView中的子View其實來來回回就那么幾個,移出屏幕的子View會很快被移入屏幕的數據重新利用起來,因而不管我們加載多少數據都不會出現OOM的情況,甚至內存都不會有所增加。
那么另外還有一點是需要大家留意的,這里獲取到了一個scrapView,然后我們在第22行將它作為第二個參數傳入到了Adapter的getView()方法當中。那么第二個參數是什么意思呢?我們再次看一下一個簡單的getView()方法示例:
~~~
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
~~~
第二個參數就是我們最熟悉的convertView呀,難怪平時我們在寫getView()方法是要判斷一下convertView是不是等于null,如果等于null才調用inflate()方法來加載布局,不等于null就可以直接利用convertView,因為convertView就是我們之間利用過的View,只不過被移出屏幕后進入到了廢棄緩存中,現在又重新拿出來使用而已。然后我們只需要把convertView中的數據更新成當前位置上應該顯示的數據,那么看起來就好像是全新加載出來的一個布局一樣,這背后的道理你是不是已經完全搞明白了?
之后的代碼又都是我們熟悉的流程了,從緩存中拿到子View之后再調用setupChild()方法將它重新attach到ListView當中,因為緩存中的View也是之前從ListView中detach掉的,這部分代碼就不再重復進行分析了。
為了方便大家理解,這里我再附上一張圖解說明:

那么到目前為止,我們就把ListView的整個工作流程代碼基本分析結束了,文章比較長,希望大家可以理解清楚,下篇文章中會講解我們平時使用ListView時遇到的問題,敬請期待。
**第一時間獲得博客更新提醒,以及更多技術信息分享,歡迎關注我的微信公眾號,掃一掃下方二維碼或搜索微信號guolin_blog,即可關注。**
[]()
- 前言
- Android觸摸屏事件派發機制詳解與源碼分析一(View篇)
- Android觸摸屏事件派發機制詳解與源碼分析二(ViewGroup篇)
- Android觸摸屏事件派發機制詳解與源碼分析三(Activity篇)
- Android應用setContentView與LayoutInflater加載解析機制源碼分析
- Android應用Context詳解及源碼解析
- Android異步消息處理機制詳解及源碼分析
- Android應用Activity、Dialog、PopWindow、Toast窗口添加機制及源碼分析
- Android ListView工作原理完全解析,帶你從源碼的角度徹底理解
- Activity啟動過程全解析
- Android應用AsyncTask處理機制詳解及源碼分析
- 說說 PendingIntent 的內部機制
- Android Activity.startActivity流程簡介
- Activity界面顯示全解析
- 框架層理解Activity生命周期(APP啟動過程)
- APK安裝過程及原理詳解
- Android構建過程簡述
- Android應用層View繪制流程與源碼分析