先來看ListView類中的makeAndAddView方法:
沒有數據變化:從mRecycler中取得可視的view
數據有變化:obtainView
1 /** 2 * 獲取視圖填充到列表的item中去,視圖可以是從未使用過的視圖轉換過來,也可以是從回收站復用的視圖。 3 * 在該方法中,先查找是否有可重用視圖,如果有,使用可重用視圖。 4 * 然后通過obtainView方法獲取一個view(有可能是從未使用視圖轉換過來 5 * (obtainView方法是在AbsListView方法中定義)),再重新測量和定位View。 6 */ 7 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 8 boolean selected) { 9 View child; 10 // 沒有數據變化:從mRecycler中取得可視的view 11 if (!mDataChanged) { 12 // Try to use an existing view for this position 13 child = mRecycler.getActiveView(position); 14 ... 15 } 16 // 生成view,回收舊view和調用mApapter.getView的地方(AbsListView) 17 child = obtainView(position, mIsScrap); 18 ... 19 return child; 20 }
第11行調用了obtainView方法,該方法的實現是在package android.widget;的AbsListView類中
1 View obtainView(int position, boolean[] isScrap) { 2 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); 3 4 isScrap[0] = false; 5 View scrapView; 6 7 scrapView = mRecycler.getTransientStateView(position); 8 if (scrapView == null) { 9 // 從回收站回收一個View 10 scrapView = mRecycler.getScrapView(position); 11 } 12 13 View child; 14 if (scrapView != null) { 15 // 這里調用了getView!注意,第二個參數也就是convertView,傳入的是剛才從回收站中回收的View(如果有的話) 16 child = mAdapter.getView(position, scrapView, this); 17 ... 18 return child; 19 }
第16行調用了getView!根據Java多態的特性,實際執行的getView將會是我們自定義BaseAdapter中的那個getView方法。
好,現在雖然找到getView的直接調用者了,問題來了,何時去觸發makeAndAddView並調用getView呢?
我們首先來看ListView中的fillDown:自頂至底去填充ListView
1 /** 2 填充從pos到list底部所有的item。里面調用到了makeAndAddView方法: 3 */ 4 private View fillDown(int pos, int nextTop) { 5 ...
6 // 這里調用到了makeAndAddView方法 7 View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); 8 ... 9 return selectedView; 10 }
有fillDown自然就有fillUp:
1 private View fillUp(int pos, int nextBottom) { 2 View selectedView = null; 3 ... 4 // 調用makeAndAddView 5 View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); 6 ... 7 return selectedView; 8 }
還有fillFromTop、fillFromMiddle、fillAboveAndBelow、fillFromSelection等,這些方法都是用來進行子元素布局的,區別是布局模式不同而已。
好了,現在布局子元素的方法有了,那么誰來觸發這些方法呢?
通過查找ListView源碼,發現剛才的那些方法在layoutChildren()中基本上都出現了。
1 @Override 2 protected void layoutChildren() { 3 ... 4 // 根據mLayoutMode的值來決定布局模式 5 switch (mLayoutMode) { 6 case LAYOUT_SET_SELECTION: 7 if (newSel != null) { 8 sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); 9 } else { 10 sel = fillFromMiddle(childrenTop, childrenBottom); 11 } 12 break; 13 case LAYOUT_SYNC: 14 sel = fillSpecific(mSyncPosition, mSpecificTop); 15 break; 16 case LAYOUT_FORCE_BOTTOM: 17 sel = fillUp(mItemCount - 1, childrenBottom); 18 adjustViewsUpOrDown(); 19 break; 20 case LAYOUT_FORCE_TOP: 21 mFirstPosition = 0; 22 sel = fillFromTop(childrenTop); 23 adjustViewsUpOrDown(); 24 break; 25 case LAYOUT_SPECIFIC: 26 sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); 27 break; 28 case LAYOUT_MOVE_SELECTION: 29 sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); 30 break; 31 default:// 默認的布局順序是從上往下 32 if (childCount == 0) { 33 if (!mStackFromBottom) { 34 final int position = lookForSelectablePosition(0, true); 35 setSelectedPositionInt(position); 36 sel = fillFromTop(childrenTop); 37 } else { 38 final int position = lookForSelectablePosition(mItemCount - 1, false); 39 setSelectedPositionInt(position); 40 sel = fillUp(mItemCount - 1, childrenBottom); 41 } 42 } else { 43 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { 44 sel = fillSpecific(mSelectedPosition, 45 oldSel == null ? childrenTop : oldSel.getTop()); 46 } else if (mFirstPosition < mItemCount) { 47 sel = fillSpecific(mFirstPosition, 48 oldFirst == null ? childrenTop : oldFirst.getTop()); 49 } else { 50 sel = fillSpecific(0, childrenTop); 51 } 52 } 53 break; 54 } 55 56 ... 57 }
繼續查找,我們發現layoutChildren的調用者是onFocusChanged、setSelectionInt、父類AbsListView中的onTouchMove、onLayout(這個比較特殊,后文會說明)等,說明當ListView的焦點發生變化時、選中某一項、或者滑動ListView時都會觸發ListView的layoutChildren()去布局子元素。
到此為止我們已經很清楚getView的調用時機了,根據掌握的知識點,我們很自然能想到,當初始化一個ListView時,getView的調用也是避免不了的。這是因為ListView在初始化時肯定會綁定一個adapter,即調用語句listview.setAdapter(adapter),我們看一下setAdapter的源碼:
1 @Override 2 public void setAdapter(ListAdapter adapter) { 3 if (mAdapter != null && mDataSetObserver != null) { 4 mAdapter.unregisterDataSetObserver(mDataSetObserver); 5 } 6 // 去除原有adapter、觀察者、選中項等信息 7 resetList(); 8 mRecycler.clear(); 9 // 包裝adapter,加header或footer,並綁定到當前ListView 10 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { 11 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); 12 } else { 13 mAdapter = adapter; 14 } 15 // 重置選中項信息 16 mOldSelectedPosition = INVALID_POSITION; 17 mOldSelectedRowId = INVALID_ROW_ID; 18 19 // AbsListView#setAdapter will update choice mode states. 20 super.setAdapter(adapter); 21 22 if (mAdapter != null) { 23 mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); 24 mOldItemCount = mItemCount; 25 mItemCount = mAdapter.getCount(); 26 checkFocus(); 27 // 重新注冊觀察者 28 mDataSetObserver = new AdapterDataSetObserver(); 29 mAdapter.registerDataSetObserver(mDataSetObserver); 30 // 設置回收器中類型不同的View數目,這里與getView的回收機制緊密相關,值得深究 31 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 32 // 設置初始選中項 33 int position; 34 if (mStackFromBottom) { 35 position = lookForSelectablePosition(mItemCount - 1, false); 36 } else { 37 position = lookForSelectablePosition(0, true); 38 } 39 setSelectedPositionInt(position); 40 setNextSelectedPositionInt(position); 41 42 if (mItemCount == 0) { 43 // Nothing selected 44 checkSelectionChanged(); 45 } 46 } else { 47 mAreAllItemsSelectable = true; 48 checkFocus(); 49 // Nothing selected 50 checkSelectionChanged(); 51 } 52 // 請求布局重繪 53 requestLayout(); 54 }
通讀setAdapter源碼,我們發現其中並未出現生成新子視圖,即調用mAdapter.getView的語句或相關方法,說明此時ListView並未包含子視圖。那么疑問來了,ListView是如何在初始化的時候生成子視圖的,也就是說第一屏的視圖是如何加載到屏幕上的?往后看,我們發現在第53行調用了requestLayout請求布局重繪,我們知道requestLayout最終會去調用onMeasure、onLayout、onDraw方法,因此我們猜測會不會是在onMeasure、onLayout、onDraw某個方法中生成了子視圖?
答案是肯定的,AbsListVIew.onLayout過程與普通視圖的layout過程完全不同,如下:
1 protected void onLayout(boolean changed, int l, int t, int r, int b) { 2 super.onLayout(changed, l, t, r, b); 3 ... 4 layoutChildren(); 5 ... 6 }
該方法調用了layoutChildren();,即重新布局ListView列表視圖。
由此說明調用requestLayout可以實現ListView列表視圖的重新布局,這里聯想到adapter.notifyDataSetChanged也會調用requestLayout,從而都能實現ListView的刷新。
以上過程只是個人探索,並非絕對正確,如有差錯敬請批評指正,謝謝。
參考文獻:
