1. 程式人生 > >Android ListView工作原理完全解析,帶你從原始碼的角度徹底理解

Android ListView工作原理完全解析,帶你從原始碼的角度徹底理解

在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(二) 。

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)) {