前言
雖然現(xiàn)在在展示數(shù)據(jù)的時候绎谦,更多的是使用 RecyclerView 而不是 ListView窃肠。但了解 ListView 還是很有必要的冤留,通過了解 ListView搀菩,既可以幫助理解更加復(fù)雜的 RecyclerView,也可以更進一步地理解 ListView 和 RecyclerView 的區(qū)別土砂。本文將基于 API28 分析 ListView 源碼谜洽。
RecycleBin
RecycleBin 是 AbsListView 中的一個內(nèi)部類阐虚,所以繼承于 AbsListView 的子類,也就是 ListView 和 GridView逊彭,都可以使用這個類侮叮。RecycleBin 機制是 ListView 能夠?qū)崿F(xiàn)成百上千條數(shù)據(jù)都不會 OOM 的一個重要原因囊榜。
類注釋
/**
* 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.
*/
通過類注釋卸勺,可以得知:
RecycleBin 有兩個重要的存儲:ActiveViews 和 ScrapViews孔庭。ActiveViews 是在布局開始時在屏幕上顯示的那些視圖圆到。在布局結(jié)束時芽淡,ActiveViews 中的所有視圖都降級為 ScrapViews挣菲。ScrapViews 是適配器可能使用的舊視圖掷邦,避免不必要地重新分配視圖抚岗。
主要成員變量
private View[] mActiveViews = new View[0];
// 之所以使用集合數(shù)組宣蔚,是因為可能有多種類型的 item胚委,同一類型的廢棄 item 放在同一 list 中
private ArrayList<View>[] mScrapViews;
// 指向 scrapViews[0](只有一種類型 item 的時候使用它)
private ArrayList<View> mCurrentScrap;
private ArrayList<View> mSkippedScrap;
主要方法
先看一下主要的方法:
- void fillActiveViews(int childCount, int firstActivePosition):第一個參數(shù)表示要存儲的 View 的數(shù)量亩冬,第二個參數(shù)表示 ListView 中第一個可見元素的索引。調(diào)用該方法后就可以根據(jù)參數(shù)將 ListView 中的指定元素存儲到 mActiveViews 數(shù)組中鸠姨。
- View getActiveView(int position):根據(jù)索引獲取相應(yīng)的 ActiveView讶迁,獲取到后就將該 View 從 ActiveViews 從移除巍糯,下次再獲取該位置的 ActiveView祟峦,將會返回 false宅楞,也就是說 ActiveView 不能被重復(fù)利用厌衙。
- void addScrapView(View scrap, int position):該方法將一個廢棄(比如滾動出了屏幕)的 View 緩存起來婶希。RecycleBin 中使用 mScrapViews 和 mCurrentScrap 來存儲廢棄的 View喻杈。
- View getScrapView(int position):根據(jù)索引找到對應(yīng)類型的 ScrapViews筒饰,并從中獲取一個 ScrapView 返回瓷们。
- public void setViewTypeCount(int viewTypeCount):Adapter 可以重寫 getViewTypeCount() 方法來表示 ListView 有幾種類型的 item换棚,而 setViewTypeCount 根據(jù)類型數(shù)來初始化 mScrapViews 數(shù)組,mCurrentScrap 指向第 0 號數(shù)組歹茶,所以如果只有一種類型,就可以使用 mCurrentScrap燎孟;如果有多種類型揩页,就使用 mScrapViews爆侣。
onLayout
View 的三大流程中兔仰,對于 ListView 而言乎赴,onMeasure 并沒有什么特別的榕吼,因為它終歸是一個 View友题,占用的空間最多也就是整個屏幕度宦。onDraw 也沒有什么意義戈抄,因為 ListView 本身并不負(fù)責(zé)繪制划鸽,繪制的任務(wù)交由子元素自己完成裸诽。ListView 大部分的神奇功能都是在 onLayout 中完成的丈冬,因此下面分析一些 ListView 的 onLayout過程埂蕊。
ListView 并沒有重寫 onLayout 方法蓄氧,重寫 onLayout 的邏輯在其父類 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 方法喉童,該方法對子元素進行布局堂氯,該方法在 AbsListView 是一個空方法祖灰,ListView 重寫了該方法:
ListView#layoutChildren
@Override
protected void layoutChildren() {
// ...
try {
// ...
final int childrenTop = mListPadding.top;
// 當(dāng)前擁有的子 View 個數(shù)恨统,第一次 layout 時子 View 個數(shù)為 0
final int childCount = getChildCount();
// ...
// 在調(diào)用 adapter.notifyDatasetChanged() 方法時畜埋,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 中保存起來
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
// 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起來
recycleBin.fillActiveViews(childCount, firstPosition);
}
// 清除所有子 View
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
// 通常情況下咖祭,mLayoutMode 為 LAYOUT_NORMAL么翰,走 default
switch (mLayoutMode) {
// ...
default:
// 第一次 onLayout 時,childCount 為 0
if (childCount == 0) {
// 判斷布局是從上往下還是從下往上码耐,默認(rè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ā)送改變時钞速,例如調(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 移動到 ScrapViews 中
recycleBin.scrapActiveViews();
// ...
// 布局完成后渴语,重置 layoutMode 和 mDataChanged
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
// ...
} // ...
}
該方法較長驾凶,只列出了主要代碼调违。重點看 switch 塊技肩,這里根據(jù) layoutMode 進行布局虚婿,一般走 default∪蝗現(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)前屏幕或全部子元素遍歷完時辛蚊,退出循環(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;
}
重點看 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;
}
}
// 通過 obtainView 方法獲取子 View
final View child = obtainView(position, mIsScrap);
// 測量和放置子 View
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
該方法先從 RecycleBin 的 ActiveViews 或通過 obtainView 方法獲取子 View真仲,再通過 setupChild 方法測量和放置子 View袋马。
第一次 layout 時秸应,RecycleBin 并沒有緩存 ActiveViews虑凛,所以只能通過 obtainView 方法獲取子 View碑宴,ListView 并沒有該方法,該方法在其父類 AbsListView 中
AbsListView#obtainView
View obtainView(int position, boolean[] isScrap) {
// ...
// 從 ScrapViews 中獲取一個 scrapView
final View scrapView = mRecycler.getScrapView(position);
// 從 Adapter 的 getView 方法獲取子 View桑谍,并將剛才得到的 scrapView 作為第二個參數(shù)傳入
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 該 scrapView 沒有被用戶利用延柠,將其返回到 ScrapViews 中
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
// ...
return child;
}
在該方法,先從 ScrapViews 中獲取一個 scrapView锣披,之后調(diào)用 Adapter 的 getView 方法獲取子 View贞间,并將剛才得到的 scrapView 作為第二個參數(shù)傳入。
在第一次 layout 中雹仿,由于 scrapView 為 null增热,所以所有的子 View 都是通過 LayoutInflater 的 inflate 方法加載出來的,相對比較耗時胧辽,不過一開始只會加載第一屏的數(shù)據(jù)峻仇,這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上。
第二次 layout
在某些手機版本中(9.0 版本好像沒有這種情況)邑商,View 在展示到界面上時會經(jīng)歷兩次 onLayout摄咆。如果 ListView 進行了兩次 onLayout 的話,就會存在一份重復(fù)的元素了奠骄。因此 ListView 在 layoutChildren 中對第二次 layout 做了處理豆同,非常巧妙地解決了這個問題。
下面就來分析一些 ListView 的第二次 layout 過程含鳞,首先看 layoutChildren 方法中的變化:
ListView#layoutChildren
@Override
protected void layoutChildren() {
// ...
try {
// ...
// 當(dāng)前擁有的子 View 個數(shù)影锈,第二次 layout 時子 View 個數(shù)不為 0
final int childCount = getChildCount();
// ...
// 將當(dāng)前所有 item 的 View 添加到 RecycleBin 的 ActiveViews 中保存起來
recycleBin.fillActiveViews(childCount, firstPosition);
// 清除所有子 View
detachAllViewsFromParent();
// 通常情況下,mLayoutMode 為 LAYOUT_NORMAL蝉绷,走 default
switch (mLayoutMode) {
// ...
default:
if (childCount == 0) {// ...}
// 第二次 layout 時進入 else 塊
else {
// 一開始沒有選中 item鸭廷,mSelectedPosition 的值為 -1
// 所以不會進入 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 中保存起來。然后清除所有舊的子 View桅狠。由于子 View 數(shù)量不為 0讼载,之后會調(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 時傳入的 position 就是第一個子 View 的位置漩符,所以和第一次 layout 的布局順序是差不多的一喘。獲取并設(shè)置子 View 還是通過 makeAndAddView 方法。
ListView#makeAndAddView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 第二次 layout 時嗜暴,可以從 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,無需再通過 inflate 方法加載子 View萎战。
小結(jié)
第一次 layout 時咐容,由于當(dāng)前子 View 數(shù)量為 0,且在 RecycleBin 的 ActiveViews 和 ScrapViews 都沒有緩存撞鹉,所以只能在 Adapter 的 getView 方法中疟丙,通過 LayoutInflate 的 inflate 方法加載子 View颖侄,相對來說比較耗時鸟雏,不過一開始只會加載第一屏的數(shù)據(jù),這樣就保證了 ListView 的內(nèi)容能夠迅速顯示在屏幕上览祖。
在某些手機版本中孝鹊,第一次顯示 ListView 時可能會發(fā)生兩次 layout。和第一次 layout 過程不同展蒂,在進行第二次 layout 時又活,子 View 數(shù)量不為 0,就可以先將所有子View 添加到 RecycleBin 的 ActiveViews 中保存起來锰悼。然后清除舊的子 View柳骄,之后再次設(shè)置新的子 View 時,由于之前已經(jīng)把舊的子 View 存到了 ActiveViews箕般,所以可以直接從 ActiveViews 中獲取到子 View耐薯,無需再通過 inflate 方法加載子 View。
(注:在 Android 9.0 版本中丝里,Button 顯示時調(diào)用了兩次 onMeasure曲初、一次 onLayout、兩次 onDraw杯聚;TextView 顯示時調(diào)用了兩次 onMeasure臼婆、一次 onLayout、一次 onDraw幌绍;ListView 會調(diào)用多次 onMeasure颁褂、一次 onLayout、多次 onDraw傀广。所以在 9.0 版本并不會發(fā)生第二次 layout颁独。)
滑動加載更多數(shù)據(jù)
上面 layout 過程分析的只是加載第一頁的數(shù)據(jù),如果有很多數(shù)據(jù)主儡,剩下的數(shù)據(jù)將會在滑動過程中加載奖唯。下面將分析一下滑動加載數(shù)據(jù)的過程。
該過程涉及到事件分發(fā)糜值,所以是從 AbsListView 的 onTouchEvent 方法開始丰捷,滑動對應(yīng) ACTION_MOVE坯墨,所以接下來調(diào)用 onTouchMove 方法,里面又有一個 switch 語句判斷 mTouchMode病往,這里對應(yīng) TOUCH_MODE_SCROLL捣染,所以接下來調(diào)用 scrollIfNeeded 方法,里面又繼續(xù)調(diào)用 trackMotionScroll 方法停巷。
下面看一下 trackMotionScroll 方法:
AbsListView#trackMotionScroll
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
// ...
// incrementalDeltaY < 0耍攘,說明是向下滑動(這里指內(nèi)容,手指是向上滑動的)
final boolean down = incrementalDeltaY < 0;
// getHeaderViewsCount 和 getFooterViewsCount 默認(rèn)返回 0
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0; // 開始移除的索引
int count = 0; // 移除的數(shù)量
// 向下滑動
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 值大于等于滑動的距離
// 說明該子 View 以及其后的 View 都在屏幕上畔勤,退出循環(huán)
if (child.getBottom() >= top) {
break;
}
// 如果該子 View 的 bottom 值小于滑動的距離蕾各,說明該子 View 已經(jīng)不在屏幕上
else {
count++;
int position = firstPosition + i; // 該子 View 的索引
// 將不在屏幕的子 View 添加進 RecycleBin 的 ScrapViews 中
if (position >= headerViewsCount && position < footerViewsStart) {
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
// 向上滑動,和向下滑動的過程相似庆揪,也是將不在屏幕上的子 View 添加進 RecycleBin 的 ScrapViews 中
// ...
}
// 將不在屏幕的子 View 全部 detach 掉
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
// 讓所有的子 View 進行相應(yīng)的偏移式曲,達到內(nèi)容隨手指的拖動而滾動的效果
offsetChildrenTopAndBottom(incrementalDeltaY);
// 向下滑動時缸榛,更新 mFirstPosition(之后填充布局時會用到)
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
// 如果第一個 View 的頂部或最后一個 View 的底部移入屏幕
// 說明要加載屏幕外的數(shù)據(jù)來填充布局
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
// ...
return false;
}
該方法首先將滑出屏幕的子 View 添加進 RecycleBin 的 ScrapViews 中,并全部 detach 掉钧排。然后讓剩下的子 View 進行相應(yīng)的偏移,達到內(nèi)容隨手指的拖動而滾動的效果恨溜。最后調(diào)用 fillGap 方法加載屏幕外的數(shù)據(jù)來填充布局负懦,fillGap 在 AbsListView 是一個抽象方法,ListView 中有具體實現(xiàn)纸厉。
ListView#fillGap
@Override
void fillGap(boolean down) {
final int count = getChildCount();
// 如果是向下滑動,就通過 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());
}
// 如果是向下滑動肯尺,就通過 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ù)滑動方向躯枢,調(diào)用 fillDown 或 fillUp 方法添加子 View,無論調(diào)用拿個方法锄蹂,最終都是調(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;
}
}
// 通過 obtainView 方法獲取子 View
final View child = obtainView(position, mIsScrap);
// 測量和放置子 View
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
先從 RecycleBin 的 ActiveViews 中獲取,如果還沒有進行第二次 layout 的話敬扛,是可以獲取到的,如果已經(jīng)進行過第二次 layout谍珊,那么就獲取不到了,因為第二次 layout 的時候已經(jīng)從 ActiveViews 中拿到過子 View砌滞,而 ActiveViews 不能重復(fù)利用,所以就獲取不到了坏怪。
如果 ActiveViews 獲取不到贝润,就會調(diào)用 obtainView 方法獲取:
AbsListView#obtainView
View obtainView(int position, boolean[] isScrap) {
// ...
// 從 ScrapViews 中獲取一個 scrapView
final View scrapView = mRecycler.getScrapView(position);
// 從 Adapter 的 getView 方法獲取子 View陕悬,并將剛才得到的 scrapView 作為第二個參數(shù)傳入
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 該 scrapView 沒有被用戶利用题暖,將其返回到 ScrapViews 中
mRecycler.addScrapView(scrapView, position);
} else {
if (child.isTemporarilyDetached()) {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
// ...
return child;
}
這次和第一次 layout 的情況不一樣,因為之前把移除屏幕的子 View 添加到了 ScrapViews 中捉超,所以現(xiàn)在就可以從 ScrapViews 中得到之前移除的子 View,并傳入 Adapter 的 getView 方法唯绍。用戶就可以利用這個緩存 View拼岳,不用再 inflate 一個子 View 了。
小結(jié)
ListView 在滑動時况芒,先將滑出屏幕的子 View 添加進 RecycleBin 的 ScrapViews 中惜纸,并從父布局中 detach 掉。然后讓剩下的子 View 進行相應(yīng)的偏移绝骚,達到內(nèi)容隨手指的拖動而滾動的效果耐版。最后通過加載屏幕外的數(shù)據(jù)來填充布局,這時就可以從 ScrapViews 中得到之前移除的子 View压汪,并傳入 Adapter 的 getView 方法粪牲。用戶就可以重復(fù)利用這個緩存 View,無需再重新 inflate 一個子 View止剖。
Adapter 相關(guān)
ListView 只是負(fù)責(zé)展示各子 View腺阳,各子 View 具體如何填充數(shù)據(jù)是交由 Adapter 來完成的。ListView 通過 setAdapter 方法和 Adapter 建立聯(lián)系穿香。先看一下該方法:
ListView#setAdapter
@Override
public void setAdapter(ListAdapter adapter) {
// 如果之前綁定過 Adapter亭引,先取消注冊 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 個數(shù)
mItemCount = mAdapter.getCount();
checkFocus();
// 生成并在 Adapter 中注冊 AdapterDataSetObserver皮获,用于通知數(shù)據(jù)源的改變
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
//...
} // ...
// 進行視圖重繪
requestLayout();
}
在該方法中焙蚓,ListView 綁定傳入的 adapter,并為 adapter 注冊 AdapterDataSetObserver,用于通知數(shù)據(jù)源的改變购公。 最后調(diào)用 requestLayout 方法赵哲,該方法最終會調(diào)用 ListView 的 onLayout,來到第一次 onLayout 的過程枫夺。
如果數(shù)據(jù)源發(fā)生了改變橡庞,想要更新 ListView 的時候扒最,我們會調(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 的父類 Observable 中:
protected final ArrayList<T> mObservers = new ArrayList<T>();
mObservers 的元素是從哪里來的呢?要從 setAdapter 的這一句說起:
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 注冊的 AdapterDataSetObserver 添加進了 mObservers 中俯渤。
所以饒了一大圈八匠,Adapter 的 notifyDataSetChanged 方法最終調(diào)用了 AdapterDataSetObserver(AdapterView 的一個內(nèi)部類)的 onChanged 方法:
AdapterDataSetObserver#onChanged
@Override
public void onChanged() {
// 將 mDataChanged 屬性設(shè)置為 true
mDataChanged = true;
mOldItemCount = mItemCount;
// 更新 item 數(shù)量
mItemCount = getAdapter().getCount();
// ...
// 最后進行視圖重繪
requestLayout();
}
在該方法中,首先將 mDataChanged 屬性設(shè)置為 true抡四,并更新 item 數(shù)量床嫌,最后進行視圖重繪厌处,在 onLayout 中更新子 View阔涉。
寫在最后
到此為止,對于 ListView 的 分析就告一段落了贯要。在分析 ListView 的過程崇渗,發(fā)現(xiàn)經(jīng)常遇到也是最重要的就是 onLayout 過程以及 RecycleBin 機制宅广。
無論是設(shè)置 Adapter 還是通知 Adapter 更新數(shù)據(jù)的過程些举,最終都會回到視圖重繪户魏,也就是 onLayout叼丑。而 onLayout 過程也會根據(jù)是第一次 layout幢码、第二次 layout 還是數(shù)據(jù)源改變的情況從不同途徑獲取子 View症副,是通過 inflate 加載還是從 RecycleBin 的 ActiveViews 或 ScrapViews 獲取贞铣。
RecycleBin 對子 View 的回收也是 ListView 的一個重點或是巧妙之處辕坝,在第二次 layout 時酱畅,會把子 View 添加到 RecycleBin 的 ActiveViews 中纺酸,之后獲取新的子 View 時就可以直接從 ActiveViews 獲取餐蔬。在滑動過程中樊诺,滑出屏幕的子 View 又會被添加到 RecycleBin 的 ScrapViews 中词爬,在之后填充布局時重新利用顿膨。