ListView 的復(fù)用機(jī)制

實現(xiàn)復(fù)用機(jī)制最關(guān)鍵的類是AbsListView.RecycleBin 類
讓我們來看看它里面關(guān)鍵的方法
mActiveViews: View[] -> 緩存當(dāng)前展示在屏幕上的子View。在布局結(jié)束時,mActiveViews中的所有視圖都移動到mScrapViews。 mActiveViews中的視圖表示連續(xù)的視圖范圍,第一個視圖存儲的位置在mFirstActivePosition中
mCurrentScrapViews: ArrayList<View> -> 若當(dāng)前 mViewTypeCount==1,也即只有一種布局類型,則直接從該列表取廢棄緩存
mScrapViews: ArrayList<View> -> 緩存不同 mViewType 的廢棄View,也即是每種不同的 ViewType,對應(yīng)不同的 scrapViews 廢棄緩存列表
mViewTypeCount: int -> 加載的列表ViewType數(shù)量
mFirstActivePosition: int -> 緩存在mActiveViews中第一個View的position
setViewTypeCount() -> 為mViewTypeCount設(shè)置childView布局類型總數(shù),并為每種類型的childView單獨啟用一個RecycleBin緩存機(jī)制。
fillActiveViews(int childCount, int firstActivePosition) -> 此方法會將ListView中的指定元素存儲到mActiveViews數(shù)組當(dāng)中。
getActiveView(int position) -> 從mActiveViews中獲取指定的元素。取出view后,在mActiveViews里的該指定位置將被置空。所以這個mActiveViews只能使用一次,并不能復(fù)用。
getScrapView(int position) -> 從廢棄緩存中取出一個View。同理,如果childView的布局類型只有一項,就直接從mCurrentScrap中取。如果多種布局,則從mScrapViews找到相對應(yīng)的緩存ArrayList再取出view。
addScrapView(View scrap, int position) -> 將一個廢棄的view進(jìn)行緩存。如果childView的布局類型只有一項,就直接緩存到mCurrentScrap。如果多種布局,則從mScrapViews找到相對應(yīng)的廢棄緩存ArrayList并緩存view。
-
ListView復(fù)用流程圖
ListView復(fù)用流程圖
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();
final Rect listPadding = mListPadding;
final int spaceAbove = listPadding.top - firstTop;
final int end = getHeight() - listPadding.bottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - getPaddingBottom() - getPaddingTop();
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
// Don't need to move views down if the top of the first position
// is already visible
return true;
}
if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
// Don't need to move views up if the bottom of the last position
// is already visible
return true;
}
final boolean down = incrementalDeltaY < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
final int top = listPadding.top - incrementalDeltaY;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
} else {
final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
invalidate();
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(getChildAt(childIndex));
}
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
awakenScrollBars();
return false;
}
- 首先我們調(diào)用trackMotionScroll()
這個方法接收兩個參數(shù),deltaY表示從手指按下時的位置到當(dāng)前手指位置的距離,incrementalDeltaY則表示據(jù)上次觸發(fā)event事件手指在Y方向上位置的改變量,那么其實我們就可以通過incrementalDeltaY的正負(fù)值情況來判斷用戶是向上還是向下滑動的了。如第34行代碼所示,如果incrementalDeltaY小于0,說明是向下滑動,否則就是向上滑動。 - 下滑過程通過if (child.getBottom() >= top)來判斷子view是不是移出屏幕外
上滑過程通過if (child.getTop() <= bottom)來判斷
如果所以會調(diào)用RecycleBin的addScrapView()方法將這個View加入到廢棄 緩存當(dāng)中,并
將count計數(shù)器加1,計數(shù)器用于記錄有多少個子View被移出了屏幕 - 接下來在第76行,會根據(jù)當(dāng)前計數(shù)器的值來進(jìn)行一個detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念當(dāng)中,所有看不到的View就沒有必要為它進(jìn)行保存,因為屏幕外還有成百上千條數(shù)據(jù)等著顯示呢,一個好的回收策略才能保證ListView的高性能和高效率。緊接著在第78行調(diào)用了offsetChildrenTopAndBottom()方法,并將incrementalDeltaY作為參數(shù)傳入,這個方法的作用是讓ListView中所有的子View都按照傳入的參數(shù)值進(jìn)行相應(yīng)的偏移,這樣就實現(xiàn)了隨著手指的拖動,ListView的內(nèi)容也會隨著滾動的效果。
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;
}
- 接下來調(diào)用makeAndAddView進(jìn)行獲取新的列表子view,如果新的列表是在之前已經(jīng)緩存在mActiveViews中就直接取出來用,否則就會調(diào)用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先調(diào)用getScrapView可能返回null或者view通過判斷是不是null來用不同的參數(shù)調(diào)用getView,這個一般要求我們自己重寫
- 再回到makeAndAddView(),執(zhí)行setupchild重新將view attach到list上
總結(jié)
-
ActivityView其實就是在UI屏幕上可見的視圖(onScreenView),也是與用戶進(jìn)行交互的View,那么這些View會通過RecycleBin直接存儲到mActivityView數(shù)組當(dāng)中,以便為了直接復(fù)用,那么當(dāng)我們滑動ListView的時候,有些View被滑動到屏幕之外(offScreen) View,那么這些View就成為了ScrapView,也就是廢棄的View,已經(jīng)無法與用戶進(jìn)行交互了,這樣在UI視圖改變的時候就沒有繪制這些無用視圖的必要了。他將會被RecycleBin存儲到mScrapView數(shù)組當(dāng)中,但是沒有被銷毀掉,目的是為了二次復(fù)用,也就是間接復(fù)用。當(dāng)新的View需要顯示的時候,先判斷mActivityView中是否存在,如果存在那么我們就可以從mActivityView數(shù)組當(dāng)中直接取出復(fù)用,也就是直接復(fù)用,否則的話從mScrapView數(shù)組當(dāng)中進(jìn)行判斷,如果存在,那么二次復(fù)用當(dāng)前的視圖,如果不存在,那么就需要inflate View了。
image.png
-我們ListView一頁可以顯示10條數(shù)據(jù),那么我們在這個時候滑動一個Item的距離,也就是說把position = 0的Item移除屏幕,將position = 10 的Item移入屏幕,那么position = 1的Item是不是就直接能夠從mActivityView數(shù)組中拿到呢?這是可以的,我們在第一次加載Item數(shù)據(jù)的時候,已經(jīng)將position = 0~9的Item加入到了mActivityView數(shù)組當(dāng)中,那么在第二次加載的時候,由于position = 1 的Item還是ActivityView,那么這里就可以直接從數(shù)組中獲取,然后重新布局。這里也就表示的是Item的直接復(fù)用。
如果我們在mActivityView數(shù)組中獲取不到position對應(yīng)的View,那么就嘗試從mScrapView廢棄View數(shù)組中嘗試去獲取,還拿剛才的例子來說當(dāng)position = 0的Item被移除屏幕的時候,首先會Detach讓View和視圖進(jìn)行分離,清空children,然后將廢棄View添加到mScrapView數(shù)組當(dāng)中,當(dāng)加載position = 10的Item時,mActivityView數(shù)組肯定是沒有的,也就無法獲取到,同樣mScrapView中也是不存在postion = 10與之對應(yīng)的廢棄View,說白了就是mScrapView數(shù)組只有mScrapView[0]這一項數(shù)據(jù),肯定是沒有mScrapView[10]這項數(shù)據(jù)的,那么我們就會這樣想,肯定是從Adapter中的getView方法獲取新的數(shù)據(jù)嘍,其實并不是這樣,雖然mScrapView中雖然沒有與之對應(yīng)的廢棄View,但是會返回最后一個緩存的View傳遞給convertview。那么也就是將mScrapView[0]對應(yīng)的View返回。總體的流程就是這樣。
image.png - ListView始終只會在getView方法中inflate一頁的Item,也就是new View只會執(zhí)行一頁Item的次數(shù)。后續(xù)的Item通過直接復(fù)用和間接復(fù)用完成。
注意一種情況:比如說還是一頁的Item,但是position = 0的Item沒有完全滑動出UI,position = 10的Item沒有完全進(jìn)入到UI的時候,那么position = 0的Item不會被detach掉,同樣不會被加入到廢棄View數(shù)組,這時mScrapView是空的,沒有任何數(shù)據(jù),那么position = 10的Item即無法從mActivityView中直接復(fù)用View,因為是第一次加載。mActivityView[10]是不存在的,同時mScrapView是空的,因此position = 10的Item只能重新生成View,也就是從getView方法中inflate


