前言
雖然現(xiàn)在在展示數(shù)據(jù)的時(shí)候,更多的是使用 RecyclerView 而不是 ListView。但了解 ListView 還是很有必要的,通過(guò)了解 ListView,既可以幫助理解更加復(fù)雜的 RecyclerView,也可以更進(jìn)一步地理解 ListView 和 RecyclerView 的區(qū)別。本文將基于 API28 分析 ListView 源碼。
RecycleBin
RecycleBin 是 AbsListView 中的一個(gè)內(nèi)部類(lèi),所以繼承于 AbsListView 的子類(lèi),也就是 ListView 和 GridView,都可以使用這個(gè)類(lèi)。RecycleBin 機(jī)制是 ListView 能夠?qū)崿F(xiàn)成百上千條數(shù)據(jù)都不會(huì) OOM 的一個(gè)重要原因。
類(lèi)注釋
/**
* 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.
*/
通過(guò)類(lèi)注釋?zhuān)梢缘弥?/p>
RecycleBin 有兩個(gè)重要的存儲(chǔ):ActiveViews 和 ScrapViews。ActiveViews 是在布局開(kāi)始時(shí)在屏幕上顯示的那些視圖。在布局結(jié)束時(shí),ActiveViews 中的所有視圖都降級(jí)為 ScrapViews。ScrapViews 是適配器可能使用的舊視圖,避免不必要地重新分配視圖。
主要成員變量
private View[] mActiveViews = new View[0];
// 之所以使用集合數(shù)組,是因?yàn)榭赡苡卸喾N類(lèi)型的 item,同一類(lèi)型的廢棄 item 放在同一 list 中
private ArrayList<View>[] mScrapViews;
// 指向 scrapViews[0](只有一種類(lèi)型 item 的時(shí)候使用它)
private ArrayList<View> mCurrentScrap;
private ArrayList<View> mSkippedScrap;
主要方法
先看一下主要的方法:
- void fillActiveViews(int childCount, int firstActivePosition):第一個(gè)參數(shù)表示要存儲(chǔ)的 View 的數(shù)量,第二個(gè)參數(shù)表示 ListView 中第一個(gè)可見(jiàn)元素的索引。調(diào)用該方法后就可以根據(jù)參數(shù)將 ListView 中的指定元素存儲(chǔ)到 mActiveViews 數(shù)組中。
- View getActiveView(int position):根據(jù)索引獲取相應(yīng)的 ActiveView,獲取到后就將該 View 從 ActiveViews 從移除,下次再獲取該位置的 ActiveView,將會(huì)返回 false,也就是說(shuō) ActiveView 不能被重復(fù)利用。
- void addScrapView(View scrap, int position):該方法將一個(gè)廢棄(比如滾動(dòng)出了屏幕)的 View 緩存起來(lái)。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 來(lái)存儲(chǔ)廢棄的 View。
- View getScrapView(int position):根據(jù)索引找到對(duì)應(yīng)類(lèi)型的 ScrapViews,并從中獲取一個(gè) ScrapView 返回。
- public void setViewTypeCount(int viewTypeCount):Adapter 可以重寫(xiě) getViewTypeCount() 方法來(lái)表示 ListView 有幾種類(lèi)型的 item,而 setViewTypeCount 根據(jù)類(lèi)型數(shù)來(lái)初始化 mScrapViews 數(shù)組,mCurrentScrap 指向第 0 號(hào)數(shù)組,所以如果只有一種類(lèi)型,就可以使用 mCurrentScrap;如果有多種類(lèi)型,就使用 mScrapViews。
onLayout
View 的三大流程中,對(duì)于 ListView 而言,onMeasure 并沒(méi)有什么特別的,因?yàn)樗K歸是一個(gè) View,占用的空間最多也就是整個(gè)屏幕。onDraw 也沒(méi)有什么意義,因?yàn)?ListView 本身并不負(fù)責(zé)繪制,繪制的任務(wù)交由子元素自己完成。ListView 大部分的神奇功能都是在 onLayout 中完成的,因此下面分析一些 ListView 的 onLayout過(guò)程。
ListView 并沒(méi)有重寫(xiě) onLayout 方法,重寫(xiě) onLayout 的邏輯在其父類(lèi) AbsListView 中:
AbsListView#onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// ...
layoutChildren();
// ...
}
主要看 layoutChildren 方法,該方法對(duì)子元素進(jìn)行布局,該方法在 AbsListView 是一個(gè)空方法,ListView 重寫(xiě)了該方法:
ListView#layoutChildren
@Override
protected void layoutChildren() {
// ...
try {
// ...
final int childrenTop = mListPadding.top;
// 當(dāng)前擁有的子 View 個(gè)數(shù),第一次 layout 時(shí)子 View 個(gè)數(shù)為 0
final int childCount = getChildCount();
// ...
// 在調(diào)用 adapter.notifyDatasetChanged() 方法時(shí),dataChanged 為 true
// 默認(rèn)情況下,dataChanged 為 false
boolean dataChanged = mDataChanged;
// ...
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
// 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ScrapViews 中保存起來(lái)
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
// 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起來(lái)
recycleBin.fillActiveViews(childCount, firstPosition);
}
// 清除所有子 View
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
// 通常情況下,mLayoutMode 為 LAYOUT_NORMAL,走 default
switch (mLayoutMode) {
// ...
default:
// 第一次 onLayout 時(shí),childCount 為 0
if (childCount == 0) {
// 判斷布局是從上往下還是從下往上,默認(rèn)為從上往下,進(jìn)入 if 塊
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);
}
}
// 非第一次 onLayout,childCount 不為 0,包括兩種情況:
// 1. 首次布局中的第二次 onLayout
// 2. 后續(xù)已經(jīng)存在子 View,但數(shù)據(jù)發(fā)送改變時(shí),例如調(diào)用了 adapter.nitifyDatasetChanged()
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;
}
// 將未使用的 ActiveViews 移動(dòng)到 ScrapViews 中
recycleBin.scrapActiveViews();
// ...
// 布局完成后,重置 layoutMode 和 mDataChanged
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
// ...
} // ...
}
該方法較長(zhǎng),只列出了主要代碼。重點(diǎn)看 switch 塊,這里根據(jù) layoutMode 進(jìn)行布局,一般走 default?,F(xiàn)在先分析第一次 onLayout 的情況,默認(rèn)從上往下布局,調(diào)用 fillFromTop 方法:
ListView#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 的合理性,之后調(diào)用了 fillDown 方法:
ListView#fillDown
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
// 當(dāng)子元素超出當(dāng)前屏幕或全部子元素遍歷完時(shí),退出循環(huán)
while (nextTop < end && pos < mItemCount) {
boolean selected = pos == mSelectedPosition;
// 添加子 View
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
重點(diǎn)看 makeAndAddView 方法,該方法用于添加子 View
ListView#makeAndAddView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 先嘗試從 ActiveView 中獲取
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// 通過(guò) obtainView 方法獲取子 View
final View child = obtainView(position, mIsScrap);
// 測(cè)量和放置子 View
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
該方法先從 RecycleBin 的 ActiveViews 或通過(guò) obtainView 方法獲取子 View,再通過(guò) setupChild 方法測(cè)量和放置子 View。
第一次 layout 時(shí),RecycleBin 并沒(méi)有緩存 ActiveViews,所以只能通過(guò) obtainView 方法獲取子 View,ListView 并沒(méi)有該方法,該方法在其父類(lèi) AbsListView 中
AbsListView#obtainView
View obtainView(int position, boolean[] isScrap) {
// ...
// 從 ScrapViews 中獲取一個(gè) scrapView
final View scrapView = mRecycler.getScrapView(position);
// 從 Adapter 的 getView 方法獲取子 View,并將剛才得到的 scrapView 作為第二個(gè)參數(shù)傳入
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 該 scrapView 沒(méi)有被用戶(hù)利用,將其返回到 ScrapViews 中
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
// ...
return child;
}
在該方法,先從 ScrapViews 中獲取一個(gè) scrapView,之后調(diào)用 Adapter 的 getView 方法獲取子 View,并將剛才得到的 scrapView 作為第二個(gè)參數(shù)傳入。
在第一次 layout 中,由于 scrapView 為 null,所以所有的子 View 都是通過(guò) LayoutInflater 的 inflate 方法加載出來(lái)的,相對(duì)比較耗時(shí),不過(guò)一開(kāi)始只會(huì)加載第一屏的數(shù)據(jù),這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上。
第二次 layout
在某些手機(jī)版本中(9.0 版本好像沒(méi)有這種情況),View 在展示到界面上時(shí)會(huì)經(jīng)歷兩次 onLayout。如果 ListView 進(jìn)行了兩次 onLayout 的話(huà),就會(huì)存在一份重復(fù)的元素了。因此 ListView 在 layoutChildren 中對(duì)第二次 layout 做了處理,非常巧妙地解決了這個(gè)問(wèn)題。
下面就來(lái)分析一些 ListView 的第二次 layout 過(guò)程,首先看 layoutChildren 方法中的變化:
ListView#layoutChildren
@Override
protected void layoutChildren() {
// ...
try {
// ...
// 當(dāng)前擁有的子 View 個(gè)數(shù),第二次 layout 時(shí)子 View 個(gè)數(shù)不為 0
final int childCount = getChildCount();
// ...
// 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起來(lái)
recycleBin.fillActiveViews(childCount, firstPosition);
// 清除所有子 View
detachAllViewsFromParent();
// 通常情況下,mLayoutMode 為 LAYOUT_NORMAL,走 default
switch (mLayoutMode) {
// ...
default:
if (childCount == 0) {// ...}
// 第二次 layout 時(shí)進(jìn)入 else 塊
else {
// 一開(kāi)始沒(méi)有選中 item,mSelectedPosition 的值為 -1
// 所以不會(huì)進(jìn)入 if 塊,而是調(diào)用 fillSpecific 方法
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;
}
// ...
} // ...
}
在第二次 layout 中,子 View 數(shù)量不為 0,所有子 View 先添加到 RecycleBin 的 ActiveViews 中保存起來(lái)。然后清除所有舊的子 View。由于子 View 數(shù)量不為 0,之后會(huì)調(diào)用 fillSpecific 方法:
ListView#fillSpecific
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
// 獲取并設(shè)置當(dāng)前 position 的子 View
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;
// 以 position 為中心,分別向上和向下獲取并設(shè)置其他子 View
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;
}
}
該方法先設(shè)置當(dāng)前 position 的子 View,然后以 position 為中心,分別向上和向下設(shè)置其他子 View。由于第二次 layout 時(shí)傳入的 position 就是第一個(gè)子 View 的位置,所以和第一次 layout 的布局順序是差不多的。獲取并設(shè)置子 View 還是通過(guò) makeAndAddView 方法。
ListView#makeAndAddView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 第二次 layout 時(shí),可以從 ActiveViews 中獲取到子 View
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// ...
}
這里和第一次 layout 不同的是,由于之前已經(jīng)把舊的子 View 存到了 ActiveViews,所以可以直接從 ActiveViews 中獲取到子 View,無(wú)需再通過(guò) inflate 方法加載子 View。
小結(jié)
第一次 layout 時(shí),由于當(dāng)前子 View 數(shù)量為 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都沒(méi)有緩存,所以只能在 Adapter 的 getView 方法中,通過(guò) LayoutInflate 的 inflate 方法加載子 View,相對(duì)來(lái)說(shuō)比較耗時(shí),不過(guò)一開(kāi)始只會(huì)加載第一屏的數(shù)據(jù),這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上。
在某些手機(jī)版本中,第一次顯示 ListView 時(shí)可能會(huì)發(fā)生兩次 layout。和第一次 layout 過(guò)程不同,在進(jìn)行第二次 layout 時(shí),子 View 數(shù)量不為 0,就可以先將所有子View 添加到 RecycleBin 的 ActiveViews 中保存起來(lái)。然后清除舊的子 View,之后再次設(shè)置新的子 View 時(shí),由于之前已經(jīng)把舊的子 View 存到了 ActiveViews,所以可以直接從 ActiveViews 中獲取到子 View,無(wú)需再通過(guò) inflate 方法加載子 View。
(注:在 Android 9.0 版本中,Button 顯示時(shí)調(diào)用了兩次 onMeasure、一次 onLayout、兩次 onDraw;TextView 顯示時(shí)調(diào)用了兩次 onMeasure、一次 onLayout、一次 onDraw;ListView 會(huì)調(diào)用多次 onMeasure、一次 onLayout、多次 onDraw。所以在 9.0 版本并不會(huì)發(fā)生第二次 layout。)
滑動(dòng)加載更多數(shù)據(jù)
上面 layout 過(guò)程分析的只是加載第一頁(yè)的數(shù)據(jù),如果有很多數(shù)據(jù),剩下的數(shù)據(jù)將會(huì)在滑動(dòng)過(guò)程中加載。下面將分析一下滑動(dòng)加載數(shù)據(jù)的過(guò)程。
該過(guò)程涉及到事件分發(fā),所以是從 AbsListView 的 onTouchEvent 方法開(kāi)始,滑動(dòng)對(duì)應(yīng) ACTION_MOVE,所以接下來(lái)調(diào)用 onTouchMove 方法,里面又有一個(gè) switch 語(yǔ)句判斷 mTouchMode,這里對(duì)應(yīng) TOUCH_MODE_SCROLL,所以接下來(lái)調(diào)用 scrollIfNeeded 方法,里面又繼續(xù)調(diào)用 trackMotionScroll 方法。
下面看一下 trackMotionScroll 方法:
AbsListView#trackMotionScroll
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
// ...
// incrementalDeltaY < 0,說(shuō)明是向下滑動(dòng)(這里指內(nèi)容,手指是向上滑動(dòng)的)
final boolean down = incrementalDeltaY < 0;
// getHeaderViewsCount 和 getFooterViewsCount 默認(rèn)返回 0
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0; // 開(kāi)始移除的索引
int count = 0; // 移除的數(shù)量
// 向下滑動(dòng)
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
// 從上往下遍歷子 View
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
// 如果該子 View 的 bottom 值大于等于滑動(dòng)的距離
// 說(shuō)明該子 View 以及其后的 View 都在屏幕上,退出循環(huán)
if (child.getBottom() >= top) {
break;
}
// 如果該子 View 的 bottom 值小于滑動(dòng)的距離,說(shuō)明該子 View 已經(jīng)不在屏幕上
else {
count++;
int position = firstPosition + i; // 該子 View 的索引
// 將不在屏幕的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中
if (position >= headerViewsCount && position < footerViewsStart) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
// 向上滑動(dòng),和向下滑動(dòng)的過(guò)程相似,也是將不在屏幕上的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中
// ...
}
// 將不在屏幕的子 View 全部 detach 掉
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
// 讓所有的子 View 進(jìn)行相應(yīng)的偏移,達(dá)到內(nèi)容隨手指的拖動(dòng)而滾動(dòng)的效果
offsetChildrenTopAndBottom(incrementalDeltaY);
// 向下滑動(dòng)時(shí),更新 mFirstPosition(之后填充布局時(shí)會(huì)用到)
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
// 如果第一個(gè) View 的頂部或最后一個(gè) View 的底部移入屏幕
// 說(shuō)明要加載屏幕外的數(shù)據(jù)來(lái)填充布局
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
// ...
return false;
}
該方法首先將滑出屏幕的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中,并全部 detach 掉。然后讓剩下的子 View 進(jìn)行相應(yīng)的偏移,達(dá)到內(nèi)容隨手指的拖動(dòng)而滾動(dòng)的效果。最后調(diào)用 fillGap 方法加載屏幕外的數(shù)據(jù)來(lái)填充布局,fillGap 在 AbsListView 是一個(gè)抽象方法,ListView 中有具體實(shí)現(xiàn)。
ListView#fillGap
@Override
void fillGap(boolean down) {
final int count = getChildCount();
// 如果是向下滑動(dòng),就通過(guò) fillDown 方法從上往下添加子 View
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
}
// 如果是向下滑動(dòng),就通過(guò) fillUp 方法從下往上添加子 View
else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
該方法根據(jù)滑動(dòng)方向,調(diào)用 fillDown 或 fillUp 方法添加子 View,無(wú)論調(diào)用拿個(gè)方法,最終都是調(diào)用 makeAndAddView 方法:
ListView#makeAndAddView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 先嘗試從 ActiveViews 中獲取
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// 通過(guò) obtainView 方法獲取子 View
final View child = obtainView(position, mIsScrap);
// 測(cè)量和放置子 View
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
先從 RecycleBin 的 ActiveViews 中獲取,如果還沒(méi)有進(jìn)行第二次 layout 的話(huà),是可以獲取到的,如果已經(jīng)進(jìn)行過(guò)第二次 layout,那么就獲取不到了,因?yàn)榈诙?layout 的時(shí)候已經(jīng)從 ActiveViews 中拿到過(guò)子 View,而 ActiveViews 不能重復(fù)利用,所以就獲取不到了。
如果 ActiveViews 獲取不到,就會(huì)調(diào)用 obtainView 方法獲?。?/p>
AbsListView#obtainView
View obtainView(int position, boolean[] isScrap) {
// ...
// 從 ScrapViews 中獲取一個(gè) scrapView
final View scrapView = mRecycler.getScrapView(position);
// 從 Adapter 的 getView 方法獲取子 View,并將剛才得到的 scrapView 作為第二個(gè)參數(shù)傳入
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 該 scrapView 沒(méi)有被用戶(hù)利用,將其返回到 ScrapViews 中
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
// ...
return child;
}
這次和第一次 layout 的情況不一樣,因?yàn)橹鞍岩瞥聊坏淖?View 添加到了 ScrapViews 中,所以現(xiàn)在就可以從 ScrapViews 中得到之前移除的子 View,并傳入 Adapter 的 getView 方法。用戶(hù)就可以利用這個(gè)緩存 View,不用再 inflate 一個(gè)子 View 了。
小結(jié)
ListView 在滑動(dòng)時(shí),先將滑出屏幕的子 View 添加進(jìn) RecycleBin 的 ScrapViews 中,并從父布局中 detach 掉。然后讓剩下的子 View 進(jìn)行相應(yīng)的偏移,達(dá)到內(nèi)容隨手指的拖動(dòng)而滾動(dòng)的效果。最后通過(guò)加載屏幕外的數(shù)據(jù)來(lái)填充布局,這時(shí)就可以從 ScrapViews 中得到之前移除的子 View,并傳入 Adapter 的 getView 方法。用戶(hù)就可以重復(fù)利用這個(gè)緩存 View,無(wú)需再重新 inflate 一個(gè)子 View。
Adapter 相關(guān)
ListView 只是負(fù)責(zé)展示各子 View,各子 View 具體如何填充數(shù)據(jù)是交由 Adapter 來(lái)完成的。ListView 通過(guò) setAdapter 方法和 Adapter 建立聯(lián)系。先看一下該方法:
ListView#setAdapter
@Override
public void setAdapter(ListAdapter adapter) {
// 如果之前綁定過(guò) Adapter,先取消注冊(cè) AdapterDataSetObserver
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
// 設(shè)置新的 Adapter
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
// 如果 ListView 有 headerView 或 footerView,需包裝傳入的 adapter
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
// 設(shè)置 item 個(gè)數(shù)
mItemCount = mAdapter.getCount();
checkFocus();
// 生成并在 Adapter 中注冊(cè) AdapterDataSetObserver,用于通知數(shù)據(jù)源的改變
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
//...
} // ...
// 進(jìn)行視圖重繪
requestLayout();
}
在該方法中,ListView 綁定傳入的 adapter,并為 adapter 注冊(cè) AdapterDataSetObserver,用于通知數(shù)據(jù)源的改變。 最后調(diào)用 requestLayout 方法,該方法最終會(huì)調(diào)用 ListView 的 onLayout,來(lái)到第一次 onLayout 的過(guò)程。
如果數(shù)據(jù)源發(fā)生了改變,想要更新 ListView 的時(shí)候,我們會(huì)調(diào)用 Adapter 的 notifyDataSetChanged 方法:
BaseAdapter#notifyDataSetChanged
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
又調(diào)用了 DataSetObservable 的 notifyChanged:
public void notifyChanged() {
synchronized(mObservers) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
這里的 mObservers 定義在 DataSetObservable 的父類(lèi) Observable 中:
protected final ArrayList<T> mObservers = new ArrayList<T>();
mObservers 的元素是從哪里來(lái)的呢?要從 setAdapter 的這一句說(shuō)起:
mAdapter.registerDataSetObserver(mDataSetObserver);
這一句最終調(diào)用了 Observable 的 registerObserver 方法:
public void registerObserver(T observer) {
if (observer == null) {
throw new IllegalArgumentException("The observer is null.");
}
synchronized(mObservers) {
if (mObservers.contains(observer)) {
throw new IllegalStateException("Observer " + observer + " is already registered.");
}
mObservers.add(observer);
}
}
可以看到,這里將 Adapter 注冊(cè)的 AdapterDataSetObserver 添加進(jìn)了 mObservers 中。
所以饒了一大圈,Adapter 的 notifyDataSetChanged 方法最終調(diào)用了 AdapterDataSetObserver(AdapterView 的一個(gè)內(nèi)部類(lèi))的 onChanged 方法:
AdapterDataSetObserver#onChanged
@Override
public void onChanged() {
// 將 mDataChanged 屬性設(shè)置為 true
mDataChanged = true;
mOldItemCount = mItemCount;
// 更新 item 數(shù)量
mItemCount = getAdapter().getCount();
// ...
// 最后進(jìn)行視圖重繪
requestLayout();
}
在該方法中,首先將 mDataChanged 屬性設(shè)置為 true,并更新 item 數(shù)量,最后進(jìn)行視圖重繪,在 onLayout 中更新子 View。
寫(xiě)在最后
到此為止,對(duì)于 ListView 的 分析就告一段落了。在分析 ListView 的過(guò)程,發(fā)現(xiàn)經(jīng)常遇到也是最重要的就是 onLayout 過(guò)程以及 RecycleBin 機(jī)制。
無(wú)論是設(shè)置 Adapter 還是通知 Adapter 更新數(shù)據(jù)的過(guò)程,最終都會(huì)回到視圖重繪,也就是 onLayout。而 onLayout 過(guò)程也會(huì)根據(jù)是第一次 layout、第二次 layout 還是數(shù)據(jù)源改變的情況從不同途徑獲取子 View,是通過(guò) inflate 加載還是從 RecycleBin 的 ActiveViews 或 ScrapViews 獲取。
RecycleBin 對(duì)子 View 的回收也是 ListView 的一個(gè)重點(diǎn)或是巧妙之處,在第二次 layout 時(shí),會(huì)把子 View 添加到 RecycleBin 的 ActiveViews 中,之后獲取新的子 View 時(shí)就可以直接從 ActiveViews 獲取。在滑動(dòng)過(guò)程中,滑出屏幕的子 View 又會(huì)被添加到 RecycleBin 的 ScrapViews 中,在之后填充布局時(shí)重新利用。