大家好,我是你們的志哥。
今天打算分享一下ListView的工作原理,覺得文章寫得不錯的朋友,歡迎點贊。更歡迎路過的大神指出其中不足。
在Android所有常用的原生控件當中,用法最複雜的應該就是ListView了,它專門用於處理那種內容元素很多,手機屏幕無法展示出所有內容的情況。ListView可以使用列表的形式來展示內容,超出屏幕部分的內容只需要通過手指滑動就可以移動到屏幕內了。
另外ListView還有一個非常神奇的功能,我相信大家應該都體驗過,即使在ListView中加載非常非常多的數據,比如達到成百上千條甚至更多,ListView都不會發生OOM或者崩潰,而且隨着我們手指滑動來瀏覽更多數據時,程序所佔用的內存竟然都不會跟着增長。那麼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中的主要代碼,如下所示:
這裏的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中實現的,代碼如下所示:
-
-
-
-
-
@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;
-
-
switch (mLayoutMode) {
-
case LAYOUT_SET_SELECTION:
-
index = mNextSelectedPosition - mFirstPosition;
-
if (index >= 0 && index < childCount) {
-
newSel = getChildAt(index);
-
}
-
break;
-
case LAYOUT_FORCE_TOP:
-
case LAYOUT_FORCE_BOTTOM:
-
case LAYOUT_SPECIFIC:
-
case LAYOUT_SYNC:
-
break;
-
case LAYOUT_MOVE_SELECTION:
-
default:
-
-
index = mSelectedPosition - mFirstPosition;
-
if (index >= 0 && index < childCount) {
-
oldSel = getChildAt(index);
-
}
-
-
oldFirst = getChildAt(0);
-
if (mNextSelectedPosition >= 0) {
-
delta = mNextSelectedPosition - mSelectedPosition;
-
}
-
-
newSel = getChildAt(index + delta);
-
}
-
boolean dataChanged = mDataChanged;
-
if (dataChanged) {
-
handleDataChanged();
-
}
-
-
-
if (mItemCount == 0) {
-
resetList();
-
invokeOnItemScrollListener();
-
return;
-
} else if (mItemCount != mAdapter.getCount()) {
-
throw new IllegalStateException("The content of the adapter has changed but "
-
+ "ListView did not receive a notification. Make sure the content of "
-
+ "your adapter is not modified from a background thread, but only "
-
+ "from the UI thread. [in ListView(" + getId() + ", " + getClass()
-
+ ") with Adapter(" + mAdapter.getClass() + ")]");
-
}
-
setSelectedPositionInt(mNextSelectedPosition);
-
-
-
final int firstPosition = mFirstPosition;
-
final RecycleBin recycleBin = mRecycler;
-
-
View focusLayoutRestoreDirectChild = null;
-
-
-
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);
-
}
-
-
-
-
-
final View focusedChild = getFocusedChild();
-
if (focusedChild != null) {
-
-
-
-
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
-
focusLayoutRestoreDirectChild = focusedChild;
-
-
focusLayoutRestoreView = findFocus();
-
if (focusLayoutRestoreView != null) {
-
-
focusLayoutRestoreView.onStartTemporaryDetach();
-
}
-
}
-
requestFocus();
-
}
-
-
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;
-
}
-
-
recycleBin.scrapActiveViews();
-
if (sel != null) {
-
-
-
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
-
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
-
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
-
if (!focusWasTaken) {
-
-
-
-
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();
-
}
-
-
-
if (hasFocus() && focusLayoutRestoreView != null) {
-
focusLayoutRestoreView.requestFocus();
-
}
-
}
-
-
-
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()方法,我們跟進去瞧一瞧:
-
-
-
-
-
-
-
-
-
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()方法,代碼如下所示:
-
-
-
-
-
-
-
-
-
-
-
-
private View fillDown(int pos, int nextTop) {
-
View selectedView = null;
-
int end = (getBottom() - getTop()) - mListPadding.bottom;
-
while (nextTop < end && pos < mItemCount) {
-
-
boolean selected = pos == mSelectedPosition;
-
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
-
nextTop = child.getBottom() + mDividerHeight;
-
if (selected) {
-
selectedView = child;
-
}
-
pos++;
-
}
-
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()方法,進入到這個方法當中,代碼如下所示:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
-
boolean selected) {
-
View child;
-
if (!mDataChanged) {
-
-
child = mRecycler.getActiveView(position);
-
if (child != null) {
-
-
-
setupChild(child, position, y, flow, childrenLeft, selected, true);
-
return child;
-
}
-
}
-
-
child = obtainView(position, mIsScrap);
-
-
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()內部到底是怎麼工作的呢?我們先進入到這個方法裏面看一下:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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()方法加載出來的,這樣就會相對比較耗時,但是不用擔心,後面就不會再有這種情況了,那麼我們繼續往下看:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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();
-
-
-
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;
-
-
switch (mLayoutMode) {
-
case LAYOUT_SET_SELECTION:
-
index = mNextSelectedPosition - mFirstPosition;
-
if (index >= 0 && index < childCount) {
-
newSel = getChildAt(index);
-
}
-
break;
-
case LAYOUT_FORCE_TOP:
-
case LAYOUT_FORCE_BOTTOM:
-
case LAYOUT_SPECIFIC:
-
case LAYOUT_SYNC:
-
break;
-
case LAYOUT_MOVE_SELECTION:
-
default:
-
-
index = mSelectedPosition - mFirstPosition;
-
if (index >= 0 && index < childCount) {
-
oldSel = getChildAt(index);
-
}
-
-
oldFirst = getChildAt(0);
-
if (mNextSelectedPosition >= 0) {
-
delta = mNextSelectedPosition - mSelectedPosition;
-
}
-
-
newSel = getChildAt(index + delta);
-
}
-
boolean dataChanged = mDataChanged;
-
if (dataChanged) {
-
handleDataChanged();
-
}
-
-
-
if (mItemCount == 0) {
-
resetList();
-
invokeOnItemScrollListener();
-
return;
-
} else if (mItemCount != mAdapter.getCount()) {
-
throw new IllegalStateException("The content of the adapter has changed but "
-
+ "ListView did not receive a notification. Make sure the content of "
-
+ "your adapter is not modified from a background thread, but only "
-
+ "from the UI thread. [in ListView(" + getId() + ", " + getClass()
-
+ ") with Adapter(" + mAdapter.getClass() + ")]");
-
}
-
setSelectedPositionInt(mNextSelectedPosition);
-
-
-
final int firstPosition = mFirstPosition;
-
final RecycleBin recycleBin = mRecycler;
-
-
View focusLayoutRestoreDirectChild = null;
-
-
-
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);
-
}
-
-
-
-
-
final View focusedChild = getFocusedChild();
-
if (focusedChild != null) {
-
-
-
-
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
-
focusLayoutRestoreDirectChild = focusedChild;
-
-
focusLayoutRestoreView = findFocus();
-
if (focusLayoutRestoreView != null) {
-
-
focusLayoutRestoreView.onStartTemporaryDetach();
-
}
-
}
-
requestFocus();
-
}
-
-
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;
-
}
-
-
recycleBin.scrapActiveViews();
-
if (sel != null) {
-
-
-
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
-
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
-
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
-
if (!focusWasTaken) {
-
-
-
-
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();
-
}
-
-
-
if (hasFocus() && focusLayoutRestoreView != null) {
-
focusLayoutRestoreView.requestFocus();
-
}
-
}
-
-
-
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()方法當中,代碼如下所示:
-
-
-
-
-
-
-
-
-
-
-
-
private View fillSpecific(int position, int top) {
-
boolean tempIsSelected = position == mSelectedPosition;
-
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
-
-
mFirstPosition = position;
-
View above;
-
View below;
-
final int dividerHeight = mDividerHeight;
-
if (!mStackFromBottom) {
-
above = fillUp(position - 1, temp.getTop() - dividerHeight);
-
-
adjustViewsUpOrDown();
-
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
-
int childCount = getChildCount();
-
if (childCount > 0) {
-
correctTooHigh(childCount);
-
}
-
} else {
-
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
-
-
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()方法,代碼如下所示:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
-
boolean selected) {
-
View child;
-
if (!mDataChanged) {
-
-
child = mRecycler.getActiveView(position);
-
if (child != null) {
-
-
-
setupChild(child, position, y, flow, childrenLeft, selected, true);
-
return child;
-
}
-
}
-
-
child = obtainView(position, mIsScrap);
-
-
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()方法當中:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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();
-
-
-
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中的這個方法:
這個方法中的代碼就非常多了,因爲它所處理的邏輯也非常多,要監聽各種各樣的觸屏事件。但是我們目前所關心的就只有手指在屏幕上滑動這一個事件而已,對應的是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) {
-
-
-
return true;
-
}
-
if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
-
-
-
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()方法是用來加載屏幕外數據的,進入到這個方法中瞧一瞧,如下所示:
-
-
-
-
-
-
-
-
-
-
abstract void fillGap(boolean down);
OK,AbsListView中的fillGap()是一個抽象方法,那麼我們立刻就能夠想到,它的具體實現肯定是在ListView中完成的了。回到ListView當中,fillGap()方法的代碼如下所示:
-
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());
-
}
-
}
down參數用於表示ListView是向下滑動還是向上滑動的,可以看到,如果是向下滑動的話就會調用fillDown()方法,而如果是向上滑動的話就會調用fillUp()方法。那麼這兩個方法我們都已經非常熟悉了,內部都是通過一個循環來去對ListView進行填充,所以這兩個方法我們就不看了,但是填充ListView會通過調用makeAndAddView()方法來完成,又是makeAndAddView()方法,但這次的邏輯再次不同了,所以我們還是回到這個方法瞧一瞧:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
-
boolean selected) {
-
View child;
-
if (!mDataChanged) {
-
-
child = mRecycler.getActiveView(position);
-
if (child != null) {
-
-
-
setupChild(child, position, y, flow, childrenLeft, selected, true);
-
return child;
-
}
-
}
-
-
child = obtainView(position, mIsScrap);
-
-
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
-
return child;
-
}
不管怎麼說,這裏首先仍然是會嘗試調用RecycleBin的getActiveView()方法來獲取子佈局,只不過肯定是獲取不到的了,因爲在第二次Layout過程中我們已經從mActiveViews中獲取過了數據,而根據RecycleBin的機制,mActiveViews是不能夠重複利用的,因此這裏返回的值肯定是null。
既然getActiveView()方法返回的值是null,那麼就還是會走到第28行的obtainView()方法當中,代碼如下所示:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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;
-
}
這裏在第19行會調用RecyleBin的getScrapView()方法來嘗試從廢棄緩存中獲取一個View,那麼廢棄緩存有沒有View呢?當然有,因爲剛纔在trackMotionScroll()方法中我們就已經看到了,一旦有任何子View被移出了屏幕,就會將它加入到廢棄緩存中,而從obtainView()方法中的邏輯來看,一旦有新的數據需要顯示到屏幕上,就會嘗試從廢棄緩存中獲取View。所以它們之間就形成了一個生產者和消費者的模式,那麼ListView神奇的地方也就在這裏體現出來了,不管你有任意多條數據需要顯示,ListView中的子View其實來來回回就那麼幾個,移出屏幕的子View會很快被移入屏幕的數據重新利用起來,因而不管我們加載多少數據都不會出現OOM的情況,甚至內存都不會有所增加。
那麼另外還有一點是需要大家留意的,這裏獲取到了一個scrapView,然後我們在第22行將它作爲第二個參數傳入到了Adapter的getView()方法當中。那麼第二個參數是什麼意思呢?我們再次看一下一個簡單的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;
-
}
第二個參數就是我們最熟悉的convertView呀,難怪平時我們在寫getView()方法是要判斷一下convertView是不是等於null,如果等於null才調用inflate()方法來加載佈局,不等於null就可以直接利用convertView,因爲convertView就是我們之間利用過的View,只不過被移出屏幕後進入到了廢棄緩存中,現在又重新拿出來使用而已。然後我們只需要把convertView中的數據更新成當前位置上應該顯示的數據,那麼看起來就好像是全新加載出來的一個佈局一樣,這背後的道理你是不是已經完全搞明白了?
之後的代碼又都是我們熟悉的流程了,從緩存中拿到子View之後再調用setupChild()方法將它重新attach到ListView當中,因爲緩存中的View也是之前從ListView中detach掉的,這部分代碼就不再重複進行分析了。
爲了方便大家理解,這裏我再附上一張圖解說明:
那麼到目前爲止,我們就把ListView的整個工作流程代碼基本分析結束了,文章比較長,希望大家可以理解清楚,下篇文章中會講解我們平時使用ListView時遇到的問題,感興趣的朋友請繼續閱讀 Android
ListView異步加載圖片亂序問題,原因分析及解決方案。
----------------------------------------------------------------------------------------------------------
理解與總結:
這張圖片很清晰地展示了ListView的工作原理,重用的思想貫穿開發大神的思想之中,因爲重用思想可以節約內存或者其他系統資源的使用,大大提高代碼的效率,減少冗餘。【其中RecycleBin能減少重複創建View對象】