何時調用getView?——從源碼的角度給出解答


先來看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的刷新。

 

 以上過程只是個人探索,並非絕對正確,如有差錯敬請批評指正,謝謝。

 參考文獻:

Android ListView初始化簡單分析

Android ListView工作原理完全解析,帶你從源碼的角度徹底理解

android源碼解析--ListView(上)

ListView源代碼分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM