現在繼續探討《深入瞭解ListView工作原理(一) – 之viewHolder優化篇》 中的第二個問題,這個問題我也有過類似困擾,當我有多種佈局給給不同的item的時候,adapter裏傳給getView裏的convertview是此position的item想要的那種viewType嗎?因爲這涉及到我是否需要在getView裏對此convertview做進一步的處理,比如判斷是否爲viewType下的不同處理方式。那麼,我們下面就從源碼的角度來尋找答案,首先我們知道getView()是被ListView父類AbsListView裏的obtainView()調用的,接下來我們先看看obtainView()是如何對convertview賦值的。
/**
* 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) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
isScrap[0] = false;
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
// If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
setItemViewLayoutParams(child, position);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}
我們這裏先不考慮transient state view的情況。那麼,從代碼可以看出,getView中的convertview參數是scrapView傳值的,所以我們接着看 mRecycler.getScrapView(position)是如何獲取convertview的。
/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
代碼很簡單,從代碼中我們可以看出,mAdapter.getItemViewType(position)就是根據postion來獲取此位置上的item是哪種viewType,而getItemViewType()就是我們自己override的函數,一般情況下如果不override BaseAdapter的getItemViewType()和 getViewTypeCount()的情況下,默認getViewTypeCount() == 1, getItemViewType(int position) == 0。
// BaseAdapter.java
public int getItemViewType(int position) {
return 0;
}
public int getViewTypeCount() {
return 1;
}
所以默認情況下就進入getScrapView()的第一個分支,但是如果我們定義了多種viewType的話,就進入第二個分支,所以接下來我們來看看mScrapViews[whichScrap]是什麼。
/**
* Unsorted views that can be used by the adapter as a convert view.
*/
private ArrayList<View>[] mScrapViews;
mScrapViews就是用來存儲RecycleBin裏用來複用的convertview的,它是一個數組,那麼它是怎麼存儲的呢,我們重點來看addScrapView:
/**
* Puts a view into the list of scrap views.
* <p>
* If the list data hasn't changed or the adapter has stable IDs, views
* with transient state will be preserved for later retrieval.
*
* @param scrap The view to add
* @param position The view's position within its parent
*/
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
// Can't recycle, but we don't know anything about the view.
// Ignore it completely.
return;
}
lp.scrappedFromPosition = position;
// Remove but don't scrap header or footer views, or views that
// should otherwise not be recycled.
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
// Can't recycle. If it's not a header or footer, which have
// special handling and should be ignored, then skip the scrap
// heap and we'll fully detach the view later.
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
scrap.dispatchStartTemporaryDetach();
// The the accessibility state of the view may change while temporary
// detached and we do not allow detached views to fire accessibility
// events. So we are announcing that the subtree changed giving a chance
// to clients holding on to a view in this subtree to refresh it.
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
// Don't scrap views that have transient state.
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
// If the adapter has stable IDs, we can reuse the view for
// the same data.
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
// If the data hasn't changed, we can reuse the views at
// their old positions.
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
// Otherwise, we'll have to remove the view and start over.
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
可以看出mScrapViews數組是的每一項是以viewType分類的,也就是說相同viewType的convertview放在數組的相同下標裏的ArrayList < View >裏,如下圖所示。
接下來我們繼續看mRecycler.getScrapView 裏的 retrieveFromScrap():
//AbsListView.java
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position or ID.
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =
(AbsListView.LayoutParams) view.getLayoutParams();
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}
可以看出,由於ArrayList< View > scrapViews 已經是根據position來找到對應viewType的convertview List,因此retrieveFromScrap只需要從中選擇一個合適即可傳遞給getView中的convertview參數。
因此,當我們override getView()時,系統已經幫我們做了預先處理,即convertview!= null 時,此被用來複用的convertview 一定是我們將要顯示的item的viewType,因此顯示的item layout 風格就是正確的,我們只需要填充對應的數據,比如圖片,文本等即可。