在Android所有常用的原生控制元件當中,用法最複雜的應該就是ListView了,它專門用於處理那種內容元素很多,手機螢幕無法展示出所有內容的情況。ListView可以使用列表的形式來展示內容,超出螢幕部分的內容只需要通過手指滑動就可以移動到螢幕內了。
另外ListView還有一個非常神奇的功能,我相信大家應該都體驗過,即使在ListView中載入非常非常多的資料,比如達到成百上千條甚至更多,ListView都不會發生OOM或者崩潰,而且隨著我們手指滑動來瀏覽更多資料時,程式所佔用的記憶體竟然都不會跟著增長。那麼ListView是怎麼實現這麼神奇的功能的呢?當初我就抱著學習的心態花了很長時間把ListView的原始碼通讀了一遍,基本瞭解了它的工作原理,在感嘆Google大神能夠寫出如此精妙程式碼的同時我也有所敬畏,因為ListView的程式碼量比較大,複雜度也很高,很難用文字表達清楚,於是我就放棄了把它寫成一篇部落格的想法。那麼現在回想起來這件事我已經腸子都悔青了,因為沒過幾個月時間我就把當初梳理清晰的原始碼又忘的一乾二淨。於是現在我又重新定下心來再次把ListView的原始碼重讀了一遍,那麼這次我一定要把它寫成一篇部落格,分享給大家的同時也當成我自己的筆記吧。
首先我們先來看一下ListView的繼承結構,如下圖所示:
可以看到,ListView的繼承結構還是相當複雜的,它是直接繼承自的AbsListView,而AbsListView有兩個子實現類,一個是ListView,另一個就是GridView,因此我們從這一點就可以猜出來,ListView和GridView在工作原理和實現上都是有很多共同點的。然後AbsListView又繼承自AdapterView,AdapterView繼承自ViewGroup,後面就是我們所熟知的了。先把ListView的繼承結構瞭解一下,待會兒有助於我們更加清晰地分析程式碼。
Adapter的作用
Adapter相信大家都不會陌生,我們平時使用ListView的時候一定都會用到它。那麼話說回來大家有沒有仔細想過,為什麼需要Adapter這個東西呢?總感覺正因為有了Adapter,ListView的使用變得要比其它控制元件複雜得多。那麼這裡我們就先來學習一下Adapter到底起到了什麼樣的一個作用。
其實說到底,控制元件就是為了互動和展示資料用的,只不過ListView更加特殊,它是為了展示很多很多資料用的,但是ListView只承擔互動和展示工作而已,至於這些資料來自哪裡,ListView是不關心的。因此,我們能設想到的最基本的ListView工作模式就是要有一個ListView控制元件和一個資料來源。
不過如果真的讓ListView和資料來源直接打交道的話,那ListView所要做的適配工作就非常繁雜了。因為資料來源這個概念太模糊了,我們只知道它包含了很多資料而已,至於這個資料來源到底是什麼樣型別,並沒有嚴格的定義,有可能是陣列,也有可能是集合,甚至有可能是資料庫表中查詢出來的遊標。所以說如果ListView真的去為每一種資料來源都進行適配操作的話,一是擴充套件性會比較差,內建了幾種適配就只有幾種適配,不能動態進行新增。二是超出了它本身應該負責的工作範圍,不再是僅僅承擔互動和展示工作就可以了,這樣ListView就會變得比較臃腫。
那麼顯然Android開發團隊是不會允許這種事情發生的,於是就有了Adapter這樣一個機制的出現。顧名思義,Adapter是介面卡的意思,它在ListView和資料來源之間起到了一個橋樑的作用,ListView並不會直接和資料來源打交道,而是會藉助Adapter這個橋樑來去訪問真正的資料來源,與之前不同的是,Adapter的介面都是統一的,因此ListView不用再去擔心任何適配方面的問題。而Adapter又是一個介面(interface),它可以去實現各種各樣的子類,每個子類都能通過自己的邏輯來去完成特定的功能,以及與特定資料來源的適配操作,比如說ArrayAdapter可以用於陣列和List型別的資料來源適配,SimpleCursorAdapter可以用於遊標型別的資料來源適配,這樣就非常巧妙地把資料來源適配困難的問題解決掉了,並且還擁有相當不錯的擴充套件性。簡單的原理示意圖如下所示:
當然Adapter的作用不僅僅只有資料來源適配這一點,還有一個非常非常重要的方法也需要我們在Adapter當中去重寫,就是getView()方法,這個在下面的文章中還會詳細講到。
RecycleBin機制
那麼在開始分析ListView的原始碼之前,還有一個東西是我們提前需要了解的,就是RecycleBin機制,這個機制也是ListView能夠實現成百上千條資料都不會OOM最重要的一個原因。其實RecycleBin的程式碼並不多,只有300行左右,它是寫在AbsListView中的一個內部類,所以所有繼承自AbsListView的子類,也就是ListView和GridView,都可以使用這個機制。那我們來看一下RecycleBin中的主要程式碼,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
/** * 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. * * <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) * <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> android.widget.AbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * Views that were on screen at the start of layout. This array is * populated at the start of layout, and at the end of layout all view * in mActiveViews are moved to mScrapViews. Views in mActiveViews * represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; /** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount * The minimum number of views mActiveViews should hold * @param firstActivePosition * The position of the first view that will be stored in * mActiveViews */ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in // active views. // However, we will NOT place them into scrap views. activeViews[i] = child; } } } /** * Get the view corresponding to the specified position. The view will * be removed from mActiveViews if it is found. * * @param position * The position to look up in mActiveViews * @return The view if it is found, null otherwise */ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >= 0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; } /** * Put a view into the ScapViews list. These views are unordered. * * @param scrap * The view to add */ void addScrapView(View scrap) { AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } // Don't put header or footer views or views that should be ignored // into the scrap heap int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(scrap, false); } return; } if (mViewTypeCount == 1) { dispatchFinishTemporaryDetach(scrap); mCurrentScrap.add(scrap); } else { dispatchFinishTemporaryDetach(scrap); mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } /** * @return A view from the ScrapViews collection. These are unordered. */ View getScrapView(int position) { ArrayList<View> scrapViews; if (mViewTypeCount == 1) { scrapViews = mCurrentScrap; int size = scrapViews.size(); if (size > 0) { return scrapViews.remove(size - 1); } else { return null; } } else { int whichScrap = mAdapter.getItemViewType(position); if (whichScrap >= 0 && whichScrap < mScrapViews.length) { scrapViews = mScrapViews[whichScrap]; int size = scrapViews.size(); if (size > 0) { return scrapViews.remove(size - 1); } } } return null; } public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } // noinspection unchecked ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList<View>(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } } |
這裡的RecycleBin程式碼並不全,我只是把最主要的幾個方法提了出來。那麼我們先來對這幾個方法進行簡單解讀,這對後面分析ListView的工作原理將會有很大的幫助。
- fillActiveViews() 這個方法接收兩個引數,第一個參數列示要儲存的view的數量,第二個參數列示ListView中第一個可見元素的position值。RecycleBin當中使用mActiveViews這個陣列來儲存View,呼叫這個方法後就會根據傳入的引數來將ListView中的指定元素儲存到mActiveViews陣列當中。
- getActiveView() 這個方法和fillActiveViews()是對應的,用於從mActiveViews陣列當中獲取資料。該方法接收一個position引數,表示元素在ListView當中的位置,方法內部會自動將position值轉換成mActiveViews陣列對應的下標值。需要注意的是,mActiveViews當中所儲存的View,一旦被獲取了之後就會從mActiveViews當中移除,下次獲取同樣位置的View將會返回null,也就是說mActiveViews不能被重複利用。
- addScrapView() 用於將一個廢棄的View進行快取,該方法接收一個View引數,當有某個View確定要廢棄掉的時候(比如滾動出了螢幕),就應該呼叫這個方法來對View進行快取,RecycleBin當中使用mScrapViews和mCurrentScrap這兩個List來儲存廢棄View。
- getScrapView 用於從廢棄快取中取出一個View,這些廢棄快取中的View是沒有順序可言的,因此getScrapView()方法中的演算法也非常簡單,就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回。
- setViewTypeCount() 我們都知道Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種型別的資料項,而setViewTypeCount()方法的作用就是為每種型別的資料項都單獨啟用一個RecycleBin快取機制。實際上,getViewTypeCount()方法通常情況下使用的並不是很多,所以我們只要知道RecycleBin當中有這樣一個功能就行了。
瞭解了RecycleBin中的主要方法以及它們的用處之後,下面就可以開始來分析ListView的工作原理了,這裡我將還是按照以前分析原始碼的方式來進行,即跟著主線執行流程來逐步閱讀並點到即止,不然的話要是把ListView所有的程式碼都貼出來,那麼本篇文章將會很長很長了。
第一次Layout
不管怎麼說,ListView即使再特殊最終還是繼承自View的,因此它的執行流程還將會按照View的規則來執行,對於這方面不太熟悉的朋友可以參考我之前寫的 Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二) 。
View的執行流程無非就分為三步,onMeasure()用於測量View的大小,onLayout()用於確定View的佈局,onDraw()用於將View繪製到介面上。而在ListView當中,onMeasure()並沒有什麼特殊的地方,因為它終歸是一個View,佔用的空間最多並且通常也就是整個螢幕。onDraw()在ListView當中也沒有什麼意義,因為ListView本身並不負責繪製,而是由ListView當中的子元素來進行繪製的。那麼ListView大部分的神奇功能其實都是在onLayout()方法中進行的了,因此我們本篇文章也是主要分析的這個方法裡的內容。
如果你到ListView原始碼中去找一找,你會發現ListView中是沒有onLayout()這個方法的,這是因為這個方法是在ListView的父類AbsListView中實現的,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * Subclasses should NOT override this method but {<a href="http://www.jobbole.com/members/57845349">@link</a> #layoutChildren()} * instead. */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mInLayout = true; if (changed) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } layoutChildren(); mInLayout = false; } |
可以看到,onLayout()方法中並沒有做什麼複雜的邏輯操作,主要就是一個判斷,如果ListView的大小或者位置發生了變化,那麼changed變數就會變成true,此時會要求所有的子佈局都強制進行重繪。除此之外倒沒有什麼難理解的地方了,不過我們注意到,在第16行呼叫了layoutChildren()這個方法,從方法名上我們就可以猜出這個方法是用來進行子元素佈局的,不過進入到這個方法當中你會發現這是個空方法,沒有一行程式碼。這當然是可以理解的了,因為子元素的佈局應該是由具體的實現類來負責完成的,而不是由父類完成。那麼進入ListView的layoutChildren()方法,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
@Override protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (!blockLayoutRequests) { mBlockLayoutRequests = true; } else { return; } try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } int childrenTop = mListPadding.top; int childrenBottom = getBottom() - getTop() - mListPadding.bottom; int childCount = getChildCount(); int index = 0; int delta = 0; View sel; View oldSel = null; View oldFirst = null; View newSel = null; View focusLayoutRestoreView = null; // Remember stuff we will need down below switch (mLayoutMode) { case LAYOUT_SET_SELECTION: index = mNextSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { newSel = getChildAt(index); } break; case LAYOUT_FORCE_TOP: case LAYOUT_FORCE_BOTTOM: case LAYOUT_SPECIFIC: case LAYOUT_SYNC: break; case LAYOUT_MOVE_SELECTION: default: // Remember the previously selected view index = mSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { oldSel = getChildAt(index); } // Remember the previous first child oldFirst = getChildAt(0); if (mNextSelectedPosition >= 0) { delta = mNextSelectedPosition - mSelectedPosition; } // Caution: newSel might be null newSel = getChildAt(index + delta); } boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } // Handle the empty set by removing all views that are visible // and calling it a day if (mItemCount == 0) { resetList(); invokeOnItemScrollListener(); return; } else if (mItemCount != mAdapter.getCount()) { throw new IllegalStateException("The content of the adapter has changed but " + "ListView did not receive a notification. Make sure the content of " + "your adapter is not modified from a background thread, but only " + "from the UI thread. [in ListView(" + getId() + ", " + getClass() + ") with Adapter(" + mAdapter.getClass() + ")]"); } setSelectedPositionInt(mNextSelectedPosition); // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; // reset the focus restoration View focusLayoutRestoreDirectChild = null; // Don't put header or footer views into the Recycler. Those are // already cached in mHeaderViews; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i)); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(getChildAt(i), ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i); } } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // take focus back to us temporarily to avoid the eventual // call to clear focus when removing the focused child below // from messing things up when ViewRoot assigns focus back // to someone else final View focusedChild = getFocusedChild(); if (focusedChild != null) { // TODO: in some cases focusedChild.getParent() == null // we can remember the focused view to restore after relayout if the // data hasn't changed, or if the focused position is a header or footer if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { focusLayoutRestoreDirectChild = focusedChild; // remember the specific view that had focus focusLayoutRestoreView = findFocus(); if (focusLayoutRestoreView != null) { // tell it we are going to mess with it focusLayoutRestoreView.onStartTemporaryDetach(); } } requestFocus(); } // Clear out old views detachAllViewsFromParent(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillFromMiddle(childrenTop, childrenBottom); } break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; default: if (childCount == 0) { 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); } } 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; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); if (sel != null) { // the current selected item should get focus if items // are focusable if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); if (!focusWasTaken) { // selected item didn't take focus, fine, but still want // to make sure something else outside of the selected view // has focus final View focused = getFocusedChild(); if (focused != null) { focused.clearFocus(); } positionSelector(sel); } else { sel.setSelected(false); mSelectorRect.setEmpty(); } } else { positionSelector(sel); } mSelectedTop = sel.getTop(); } else { if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) positionSelector(child); } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } // even if there is not selected position, we may need to restore // focus (i.e. something focusable in touch mode) if (hasFocus() && focusLayoutRestoreView != null) { focusLayoutRestoreView.requestFocus(); } } // tell focus view we are done mucking with it, if it is still in // our view hierarchy. if (focusLayoutRestoreView != null && focusLayoutRestoreView.getWindowToken() != null) { focusLayoutRestoreView.onFinishTemporaryDetach(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } } |
這段程式碼比較長,我們挑重點的看。首先可以確定的是,ListView當中目前還沒有任何子View,資料都還是由Adapter管理的,並沒有展示到介面上,因此第19行getChildCount()方法得到的值肯定是0。接著在第81行會根據dataChanged這個布林型的值來判斷執行邏輯,dataChanged只有在資料來源發生改變的情況下才會變成true,其它情況都是false,因此這裡會進入到第90行的執行邏輯,呼叫RecycleBin的fillActiveViews()方法。按理來說,呼叫fillActiveViews()方法是為了將ListView的子View進行快取的,可是目前ListView中還沒有任何的子View,因此這一行暫時還起不了任何作用。
接下來在第114行會根據mLayoutMode的值來決定佈局模式,預設情況下都是普通模式LAYOUT_NORMAL,因此會進入到第140行的default語句當中。而下面又會緊接著進行兩次if判斷,childCount目前是等於0的,並且預設的佈局順序是從上往下,因此會進入到第145行的fillFromTop()方法,我們跟進去瞧一瞧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * Fills the list from top to bottom, starting with mFirstPosition * * @param nextTop The location where the top of the first item should be * drawn * * @return The view that is currently selected */ 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開始,自頂至底去填充ListView。而這個方法本身並沒有什麼邏輯,就是判斷了一下mFirstPosition值的合法性,然後呼叫fillDown()方法,那麼我們就有理由可以猜測,填充ListView的操作是在fillDown()方法中完成的。進入fillDown()方法,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * Fills the list from pos down to the end of the list view. * * @param pos The first position to put in the list * * @param nextTop The location where the top of the item associated with pos * should be drawn * * @return The view that is currently selected, if it happens to be in the * range that we draw. */ private View fillDown(int pos, int nextTop) { View selectedView = null; int end = (getBottom() - getTop()) - mListPadding.bottom; while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; } return selectedView; } |
可以看到,這裡使用了一個while迴圈來執行重複邏輯,一開始nextTop的值是第一個子元素頂部距離整個ListView頂部的畫素值,pos則是剛剛傳入的mFirstPosition的值,而end是ListView底部減去頂部所得的畫素值,mItemCount則是Adapter中的元素數量。因此一開始的情況下nextTop必定是小於end值的,並且pos也是小於mItemCount值的。那麼每執行一次while迴圈,pos的值都會加1,並且nextTop也會增加,當nextTop大於等於end時,也就是子元素已經超出當前螢幕了,或者pos大於等於mItemCount時,也就是所有Adapter中的元素都被遍歷結束了,就會跳出while迴圈。
那麼while迴圈當中又做了什麼事情呢?值得讓人留意的就是第18行呼叫的makeAndAddView()方法,進入到這個方法當中,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ 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; } |
這裡在第19行嘗試從RecycleBin當中快速獲取一個active view,不過很遺憾的是目前RecycleBin當中還沒有快取任何的View,所以這裡得到的值肯定是null。那麼取得了null之後就會繼續向下執行,到第28行會呼叫obtainView()方法來再次嘗試獲取一個View,這次的obtainView()方法是可以保證一定返回一個View的,於是下面立刻將獲取到的View傳入到了setupChild()方法當中。那麼obtainView()內部到底是怎麼工作的呢?我們先進入到這個方法裡面看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/** * 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) { 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()方法中的程式碼並不多,但卻包含了非常非常重要的邏輯,不誇張的說,整個ListView中最重要的內容可能就在這個方法裡了。那麼我們還是按照執行流程來看,在第19行程式碼中呼叫了RecycleBin的getScrapView()方法來嘗試獲取一個廢棄快取中的View,同樣的道理,這裡肯定是獲取不到的,getScrapView()方法會返回一個null。這時該怎麼辦呢?沒有關係,程式碼會執行到第33行,呼叫mAdapter的getView()方法來去獲取一個View。那麼mAdapter是什麼呢?當然就是當前ListView關聯的介面卡了。而getView()方法又是什麼呢?還用說嗎,這個就是我們平時使用ListView時最最經常重寫的一個方法了,這裡getView()方法中傳入了三個引數,分別是position,null和this。
那麼我們平時寫ListView的Adapter時,getView()方法通常會怎麼寫呢?這裡我舉個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getItem(position); View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(resourceId, null); } else { view = convertView; } ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image); TextView fruitName = (TextView) view.findViewById(R.id.fruit_name); fruitImage.setImageResource(fruit.getImageId()); fruitName.setText(fruit.getName()); return view; } |
getView()方法接受的三個引數,第一個引數position代表當前子元素的的位置,我們可以通過具體的位置來獲取與其相關的資料。第二個引數convertView,剛才傳入的是null,說明沒有convertView可以利用,因此我們會呼叫LayoutInflater的inflate()方法來去載入一個佈局。接下來會對這個view進行一些屬性和值的設定,最後將view返回。
那麼這個View也會作為obtainView()的結果進行返回,並最終傳入到setupChild()方法當中。其實也就是說,第一次layout過程當中,所有的子View都是呼叫LayoutInflater的inflate()方法載入出來的,這樣就會相對比較耗時,但是不用擔心,後面就不會再有這種情況了,那麼我們繼續往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/** * Add a view as a child and make sure it is measured (if necessary) and * positioned properly. * * @param child The view to add * @param position The position of this child * @param y The y position relative to which this view will be positioned * @param flowDown If true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled Has this view been pulled from the recycle bin? If so it * does not need to be remeasured. */ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) { final boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make some up... // noinspection unchecked AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); } if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (needToMeasure) { int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childTop = flowDown ? y : y - h; if (needToMeasure) { final int childRight = childrenLeft + w; final int childBottom = childTop + h; child.layout(childrenLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childrenLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted && !child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(true); } } |
setupChild()方法當中的程式碼雖然比較多,但是我們只看核心程式碼的話就非常簡單了,剛才呼叫obtainView()方法獲取到的子元素View,這裡在第40行呼叫了addViewInLayout()方法將它新增到了ListView當中。那麼根據fillDown()方法中的while迴圈,會讓子元素View將整個ListView控制元件填滿然後就跳出,也就是說即使我們的Adapter中有一千條資料,ListView也只會載入第一屏的資料,剩下的資料反正目前在螢幕上也看不到,所以不會去做多餘的載入工作,這樣就可以保證ListView中的內容能夠迅速展示到螢幕上。
那麼到此為止,第一次Layout過程結束。
第二次Layout
雖然我在原始碼中並沒有找出具體的原因,但如果你自己做一下實驗的話就會發現,即使是一個再簡單的View,在展示到介面上之前都會經歷至少兩次onMeasure()和兩次onLayout()的過程。其實這只是一個很小的細節,平時對我們影響並不大,因為不管是onMeasure()或者onLayout()幾次,反正都是執行的相同的邏輯,我們並不需要進行過多關心。但是在ListView中情況就不一樣了,因為這就意味著layoutChildren()過程會執行兩次,而這個過程當中涉及到向ListView中新增子元素,如果相同的邏輯執行兩遍的話,那麼ListView中就會存在一份重複的資料了。因此ListView在layoutChildren()過程當中做了第二次Layout的邏輯處理,非常巧妙地解決了這個問題,下面我們就來分析一下第二次Layout的過程。
其實第二次Layout和第一次Layout的基本流程是差不多的,那麼我們還是從layoutChildren()方法開始看起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
@Override protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (!blockLayoutRequests) { mBlockLayoutRequests = true; } else { return; } try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } int childrenTop = mListPadding.top; int childrenBottom = getBottom() - getTop() - mListPadding.bottom; int childCount = getChildCount(); int index = 0; int delta = 0; View sel; View oldSel = null; View oldFirst = null; View newSel = null; View focusLayoutRestoreView = null; // Remember stuff we will need down below switch (mLayoutMode) { case LAYOUT_SET_SELECTION: index = mNextSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { newSel = getChildAt(index); } break; case LAYOUT_FORCE_TOP: case LAYOUT_FORCE_BOTTOM: case LAYOUT_SPECIFIC: case LAYOUT_SYNC: break; case LAYOUT_MOVE_SELECTION: default: // Remember the previously selected view index = mSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { oldSel = getChildAt(index); } // Remember the previous first child oldFirst = getChildAt(0); if (mNextSelectedPosition >= 0) { delta = mNextSelectedPosition - mSelectedPosition; } // Caution: newSel might be null newSel = getChildAt(index + delta); } boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } // Handle the empty set by removing all views that are visible // and calling it a day if (mItemCount == 0) { resetList(); invokeOnItemScrollListener(); return; } else if (mItemCount != mAdapter.getCount()) { throw new IllegalStateException("The content of the adapter has changed but " + "ListView did not receive a notification. Make sure the content of " + "your adapter is not modified from a background thread, but only " + "from the UI thread. [in ListView(" + getId() + ", " + getClass() + ") with Adapter(" + mAdapter.getClass() + ")]"); } setSelectedPositionInt(mNextSelectedPosition); // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; // reset the focus restoration View focusLayoutRestoreDirectChild = null; // Don't put header or footer views into the Recycler. Those are // already cached in mHeaderViews; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i)); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(getChildAt(i), ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i); } } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // take focus back to us temporarily to avoid the eventual // call to clear focus when removing the focused child below // from messing things up when ViewRoot assigns focus back // to someone else final View focusedChild = getFocusedChild(); if (focusedChild != null) { // TODO: in some cases focusedChild.getParent() == null // we can remember the focused view to restore after relayout if the // data hasn't changed, or if the focused position is a header or footer if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { focusLayoutRestoreDirectChild = focusedChild; // remember the specific view that had focus focusLayoutRestoreView = findFocus(); if (focusLayoutRestoreView != null) { // tell it we are going to mess with it focusLayoutRestoreView.onStartTemporaryDetach(); } } requestFocus(); } // Clear out old views detachAllViewsFromParent(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillFromMiddle(childrenTop, childrenBottom); } break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); break; case LAYOUT_MOVE_SELECTION: sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); break; default: if (childCount == 0) { 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); } } 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; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); if (sel != null) { // the current selected item should get focus if items // are focusable if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); if (!focusWasTaken) { // selected item didn't take focus, fine, but still want // to make sure something else outside of the selected view // has focus final View focused = getFocusedChild(); if (focused != null) { focused.clearFocus(); } positionSelector(sel); } else { sel.setSelected(false); mSelectorRect.setEmpty(); } } else { positionSelector(sel); } mSelectedTop = sel.getTop(); } else { if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) positionSelector(child); } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } // even if there is not selected position, we may need to restore // focus (i.e. something focusable in touch mode) if (hasFocus() && focusLayoutRestoreView != null) { focusLayoutRestoreView.requestFocus(); } } // tell focus view we are done mucking with it, if it is still in // our view hierarchy. if (focusLayoutRestoreView != null && focusLayoutRestoreView.getWindowToken() != null) { focusLayoutRestoreView.onFinishTemporaryDetach(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } } |
同樣還是在第19行,呼叫getChildCount()方法來獲取子View的數量,只不過現在得到的值不會再是0了,而是ListView中一屏可以顯示的子View數量,因為我們剛剛在第一次Layout過程當中向ListView新增了這麼多的子View。下面在第90行呼叫了RecycleBin的fillActiveViews()方法,這次效果可就不一樣了,因為目前ListView中已經有子View了,這樣所有的子View都會被快取到RecycleBin的mActiveViews陣列當中,後面將會用到它們。
接下來將會是非常非常重要的一個操作,在第113行呼叫了detachAllViewsFromParent()方法。這個方法會將所有ListView當中的子View全部清除掉,從而保證第二次Layout過程不會產生一份重複的資料。那有的朋友可能會問了,這樣把已經載入好的View又清除掉,待會還要再重新載入一遍,這不是嚴重影響效率嗎?不用擔心,還記得我們剛剛呼叫了RecycleBin的fillActiveViews()方法來快取子View嗎,待會兒將會直接使用這些快取好的View來進行載入,而並不會重新執行一遍inflate過程,因此效率方面並不會有什麼明顯的影響。
那麼我們接著看,在第141行的判斷邏輯當中,由於不再等於0了,因此會進入到else語句當中。而else語句中又有三個邏輯判斷,第一個邏輯判斷不成立,因為預設情況下我們沒有選中任何子元素,mSelectedPosition應該等於-1。第二個邏輯判斷通常是成立的,因為mFirstPosition的值一開始是等於0的,只要adapter中的資料大於0條件就成立。那麼進入到fillSpecific()方法當中,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/** * Put a specific item at a specific location on the screen and then build * up and down from there. * * @param position The reference view to use as the starting point * @param top Pixel offset from the top of this view to the top of the * reference view. * * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific(int position, int top) { boolean tempIsSelected = position == mSelectedPosition; 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; final int dividerHeight = mDividerHeight; if (!mStackFromBottom) { above = fillUp(position - 1, temp.getTop() - dividerHeight); // This will correct for the top of the first view not touching the top of the list adjustViewsUpOrDown(); below = fillDown(position + 1, temp.getBottom() + dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooHigh(childCount); } } else { below = fillDown(position + 1, temp.getBottom() + dividerHeight); // This will correct for the bottom of the last view not touching the bottom of the list 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; } } |
fillSpecific()這算是一個新方法了,不過其實它和fillUp()、fillDown()方法功能也是差不多的,主要的區別在於,fillSpecific()方法會優先將指定位置的子View先載入到螢幕上,然後再載入該子View往上以及往下的其它子View。那麼由於這裡我們傳入的position就是第一個子View的位置,於是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,這裡我們就不去關注太多它的細節,而是將精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ 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; } |
仍然還是在第19行嘗試從RecycleBin當中獲取Active View,然而這次就一定可以獲取到了,因為前面我們呼叫了RecycleBin的fillActiveViews()方法來快取子View。那麼既然如此,就不會再進入到第28行的obtainView()方法,而是會直接進入setupChild()方法當中,這樣也省去了很多時間,因為如果在obtainView()方法中又要去infalte佈局的話,那麼ListView的初始載入效率就大大降低了。
注意在第23行,setupChild()方法的最後一個引數傳入的是true,這個參數列明當前的View是之前被回收過的,那麼我們再次回到setupChild()方法當中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/** * Add a view as a child and make sure it is measured (if necessary) and * positioned properly. * * @param child The view to add * @param position The position of this child * @param y The y position relative to which this view will be positioned * @param flowDown If true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled Has this view been pulled from the recycle bin? If so it * does not need to be remeasured. */ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) { final boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make some up... // noinspection unchecked AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); } if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (needToMeasure) { int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childTop = flowDown ? y : y - h; if (needToMeasure) { final int childRight = childrenLeft + w; final int childBottom = childTop + h; child.layout(childrenLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childrenLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted && !child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(true); } } |
可以看到,setupChild()方法的最後一個引數是recycled,然後在第32行會對這個變數進行判斷,由於recycled現在是true,所以會執行attachViewToParent()方法,而第一次Layout過程則是執行的else語句中的addViewInLayout()方法。這兩個方法最大的區別在於,如果我們需要向ViewGroup中新增一個新的子View,應該呼叫addViewInLayout()方法,而如果是想要將一個之前detach的View重新attach到ViewGroup上,就應該呼叫attachViewToParent()方法。那麼由於前面在layoutChildren()方法當中呼叫了detachAllViewsFromParent()方法,這樣ListView中所有的子View都是處於detach狀態的,所以這裡attachViewToParent()方法是正確的選擇。
經歷了這樣一個detach又attach的過程,ListView中所有的子View又都可以正常顯示出來了,那麼第二次Layout過程結束。
滑動載入更多資料
經歷了兩次Layout過程,雖說我們已經可以在ListView中看到內容了,然而關於ListView最神奇的部分我們卻還沒有接觸到,因為目前ListView中只是載入並顯示了第一屏的資料而已。比如說我們的Adapter當中有1000條資料,但是第一屏只顯示了10條,ListView中也只有10個子View而已,那麼剩下的990是怎樣工作並顯示到介面上的呢?這就要看一下ListView滑動部分的原始碼了,因為我們是通過手指滑動來顯示更多資料的。
由於滑動部分的機制是屬於通用型的,即ListView和GridView都會使用同樣的機制,因此這部分程式碼就肯定是寫在AbsListView當中的了。那麼監聽觸控事件是在onTouchEvent()方法當中進行的,我們就來看一下AbsListView中的這個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
@Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled()) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isClickable() || isLongClickable(); } final int action = ev.getAction(); View v; int deltaY; if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { mActivePointerId = ev.getPointerId(0); final int x = (int) ev.getX(); final int y = (int) ev.getY(); int motionPosition = pointToPosition(x, y); if (!mDataChanged) { if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) && (getAdapter().isEnabled(motionPosition))) { // User clicked on an actual view (and was not stopping a // fling). It might be a // click or a scroll. Assume it is a click until proven // otherwise mTouchMode = TOUCH_MODE_DOWN; // FIXME Debounce if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { if (ev.getEdgeFlags() != 0 && motionPosition < 0) { // If we couldn't find a view to click on, but the down // event was touching // the edge, we will bail out and try again. This allows // the edge correcting // code in ViewRoot to try to find a nearby view to // select return false; } if (mTouchMode == TOUCH_MODE_FLING) { // Stopped a fling. It is a scroll. createScrollingCache(); mTouchMode = TOUCH_MODE_SCROLL; mMotionCorrection = 0; motionPosition = findMotionRow(y); reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } } if (motionPosition >= 0) { // Remember where the motion event started v = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = v.getTop(); } mMotionX = x; mMotionY = y; mMotionPosition = motionPosition; mLastY = Integer.MIN_VALUE; break; } case MotionEvent.ACTION_MOVE: { final int pointerIndex = ev.findPointerIndex(mActivePointerId); final int y = (int) ev.getY(pointerIndex); deltaY = y - mMotionY; switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: // Check if we have moved far enough that it looks more like a // scroll than a tap startScrollIfNeeded(deltaY); break; case TOUCH_MODE_SCROLL: if (PROFILE_SCROLLING) { if (!mScrollProfilingStarted) { Debug.startMethodTracing("AbsListViewScroll"); mScrollProfilingStarted = true; } } if (y != mLastY) { deltaY -= mMotionCorrection; int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; // No need to do all this work if we're not going to move // anyway boolean atEdge = false; if (incrementalDeltaY != 0) { atEdge = trackMotionScroll(deltaY, incrementalDeltaY); } // Check to see if we have bumped into the scroll limit if (atEdge && getChildCount() > 0) { // Treat this like we're starting a new scroll from the // current // position. This will let the user start scrolling back // into // content immediately rather than needing to scroll // back to the // point where they hit the limit first. int motionPosition = findMotionRow(y); if (motionPosition >= 0) { final View motionView = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = motionView.getTop(); } mMotionY = y; mMotionPosition = motionPosition; invalidate(); } mLastY = y; } break; } break; } case MotionEvent.ACTION_UP: { switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: final int motionPosition = mMotionPosition; final View child = getChildAt(motionPosition - mFirstPosition); if (child != null && !child.hasFocusable()) { if (mTouchMode != TOUCH_MODE_DOWN) { child.setPressed(false); } if (mPerformClick == null) { mPerformClick = new PerformClick(); } final AbsListView.PerformClick performClick = mPerformClick; performClick.mChild = child; performClick.mClickMotionPosition = motionPosition; performClick.rememberWindowAttachCount(); mResurrectToPosition = motionPosition; if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); } mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { mTouchMode = TOUCH_MODE_TAP; setSelectedPositionInt(mMotionPosition); layoutChildren(); child.setPressed(true); positionSelector(child); setPressed(true); if (mSelector != null) { Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { ((TransitionDrawable) d).resetTransition(); } } postDelayed(new Runnable() { public void run() { child.setPressed(false); setPressed(false); if (!mDataChanged) { post(performClick); } mTouchMode = TOUCH_MODE_REST; } }, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; } return true; } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { post(performClick); } } mTouchMode = TOUCH_MODE_REST; break; case TOUCH_MODE_SCROLL: final int childCount = getChildCount(); if (childCount > 0) { if (mFirstPosition == 0 && getChildAt(0).getTop() >= mListPadding.top && mFirstPosition + childCount < mItemCount && getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom) { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); final int initialVelocity = (int) velocityTracker .getYVelocity(mActivePointerId); if (Math.abs(initialVelocity) > mMinimumVelocity) { if (mFlingRunnable == null) { mFlingRunnable = new FlingRunnable(); } reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); mFlingRunnable.start(-initialVelocity); } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } break; } setPressed(false); // Need to redraw since we probably aren't drawing the selector // anymore invalidate(); final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mActivePointerId = INVALID_POINTER; if (PROFILE_SCROLLING) { if (mScrollProfilingStarted) { Debug.stopMethodTracing(); mScrollProfilingStarted = false; } } break; } case MotionEvent.ACTION_CANCEL: { mTouchMode = TOUCH_MODE_REST; setPressed(false); View motionView = this.getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } clearScrollingCache(); final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mActivePointerId = INVALID_POINTER; break; } case MotionEvent.ACTION_POINTER_UP: { onSecondaryPointerUp(ev); final int x = mMotionX; final int y = mMotionY; final int motionPosition = pointToPosition(x, y); if (motionPosition >= 0) { // Remember where the motion event started v = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = v.getTop(); mMotionPosition = motionPosition; } mLastY = y; break; } } return true; } |
這個方法中的程式碼就非常多了,因為它所處理的邏輯也非常多,要監聽各種各樣的觸屏事件。但是我們目前所關心的就只有手指在螢幕上滑動這一個事件而已,對應的是ACTION_MOVE這個動作,那麼我們就只看這部分程式碼就可以了。
可以看到,ACTION_MOVE這個case裡面又巢狀了一個switch語句,是根據當前的TouchMode來選擇的。那這裡我可以直接告訴大家,當手指在螢幕上滑動時,TouchMode是等於TOUCH_MODE_SCROLL這個值的,至於為什麼那又要牽扯到另外的好幾個方法,這裡限於篇幅原因就不再展開講解了,喜歡尋根究底的朋友們可以自己去原始碼裡找一找原因。
這樣的話,程式碼就應該會走到第78行的這個case裡面去了,在這個case當中並沒有什麼太多需要注意的東西,唯一一點非常重要的就是第92行呼叫的trackMotionScroll()方法,相當於我們手指只要在螢幕上稍微有一點點移動,這個方法就會被呼叫,而如果是正常在螢幕上滑動的話,那麼這個方法就會被呼叫很多次。那麼我們進入到這個方法中瞧一瞧,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
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; } |
這個方法接收兩個引數,deltaY表示從手指按下時的位置到當前手指位置的距離,incrementalDeltaY則表示據上次觸發event事件手指在Y方向上位置的改變數,那麼其實我們就可以通過incrementalDeltaY的正負值情況來判斷使用者是向上還是向下滑動的了。如第34行程式碼所示,如果incrementalDeltaY小於0,說明是向下滑動,否則就是向上滑動。
下面將會進行一個邊界值檢測的過程,可以看到,從第43行開始,當ListView向下滑動的時候,就會進入一個for迴圈當中,從上往下依次獲取子View,第47行當中,如果該子View的bottom值已經小於top值了,就說明這個子View已經移出螢幕了,所以會呼叫RecycleBin的addScrapView()方法將這個View加入到廢棄快取當中,並將count計數器加1,計數器用於記錄有多少個子View被移出了螢幕。那麼如果是ListView向上滑動的話,其實過程是基本相同的,只不過變成了從下往上依次獲取子View,然後判斷該子View的top值是不是大於bottom值了,如果大於的話說明子View已經移出了螢幕,同樣把它加入到廢棄快取中,並將計數器加1。
接下來在第76行,會根據當前計數器的值來進行一個detach操作,它的作用就是把所有移出螢幕的子View全部detach掉,在ListView的概念當中,所有看不到的View就沒有必要為它進行儲存,因為螢幕外還有成百上千條資料等著顯示呢,一個好的回收策略才能保證ListView的高效能和高效率。緊接著在第78行呼叫了offsetChildrenTopAndBottom()方法,並將incrementalDeltaY作為引數傳入,這個方法的作用是讓ListView中所有的子View都按照傳入的引數值進行相應的偏移,這樣就實現了隨著手指的拖動,ListView的內容也會隨著滾動的效果。
然後在第84行會進行判斷,如果ListView中最後一個View的底部已經移入了螢幕,或者ListView中第一個View的頂部移入了螢幕,就會呼叫fillGap()方法,那麼因此我們就可以猜出fillGap()方法是用來載入螢幕外資料的,進入到這個方法中瞧一瞧,如下所示:
1 2 3 4 5 6 7 8 9 10 |
/** * Fills the gap left open by a touch-scroll. During a touch scroll, * children that remain on screen are shifted and the other ones are * discarded. The role of this method is to fill the gap thus created by * performing a partial layout in the empty space. * * @param down * true if the scroll is going down, false if it is going up */ abstract void fillGap(boolean down); |
OK,AbsListView中的fillGap()是一個抽象方法,那麼我們立刻就能夠想到,它的具體實現肯定是在ListView中完成的了。回到ListView當中,fillGap()方法的程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void fillGap(boolean down) { final int count = getChildCount(); if (down) { final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : getListPaddingTop(); fillDown(mFirstPosition + count, startOffset); correctTooHigh(getChildCount()); } else { final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - getListPaddingBottom(); fillUp(mFirstPosition - 1, startOffset); correctTooLow(getChildCount()); } } |
down引數用於表示ListView是向下滑動還是向上滑動的,可以看到,如果是向下滑動的話就會呼叫fillDown()方法,而如果是向上滑動的話就會呼叫fillUp()方法。那麼這兩個方法我們都已經非常熟悉了,內部都是通過一個迴圈來去對ListView進行填充,所以這兩個方法我們就不看了,但是填充ListView會通過呼叫makeAndAddView()方法來完成,又是makeAndAddView()方法,但這次的邏輯再次不同了,所以我們還是回到這個方法瞧一瞧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ 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; } |
不管怎麼說,這裡首先仍然是會嘗試呼叫RecycleBin的getActiveView()方法來獲取子佈局,只不過肯定是獲取不到的了,因為在第二次Layout過程中我們已經從mActiveViews中獲取過了資料,而根據RecycleBin的機制,mActiveViews是不能夠重複利用的,因此這裡返回的值肯定是null。
既然getActiveView()方法返回的值是null,那麼就還是會走到第28行的obtainView()方法當中,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/** * 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) { 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; } |
這裡在第19行會呼叫RecyleBin的getScrapView()方法來嘗試從廢棄快取中獲取一個View,那麼廢棄快取有沒有View呢?當然有,因為剛才在trackMotionScroll()方法中我們就已經看到了,一旦有任何子View被移出了螢幕,就會將它加入到廢棄快取中,而從obtainView()方法中的邏輯來看,一旦有新的資料需要顯示到螢幕上,就會嘗試從廢棄快取中獲取View。所以它們之間就形成了一個生產者和消費者的模式,那麼ListView神奇的地方也就在這裡體現出來了,不管你有任意多條資料需要顯示,ListView中的子View其實來來回回就那麼幾個,移出螢幕的子View會很快被移入螢幕的資料重新利用起來,因而不管我們載入多少資料都不會出現OOM的情況,甚至記憶體都不會有所增加。
那麼另外還有一點是需要大家留意的,這裡獲取到了一個scrapView,然後我們在第22行將它作為第二個引數傳入到了Adapter的getView()方法當中。那麼第二個引數是什麼意思呢?我們再次看一下一個簡單的getView()方法示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public View getView(int position, View convertView, ViewGroup parent) { Fruit fruit = getItem(position); View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(resourceId, null); } else { view = convertView; } ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image); TextView fruitName = (TextView) view.findViewById(R.id.fruit_name); fruitImage.setImageResource(fruit.getImageId()); fruitName.setText(fruit.getName()); return view; } |
第二個引數就是我們最熟悉的convertView呀,難怪平時我們在寫getView()方法是要判斷一下convertView是不是等於null,如果等於null才呼叫inflate()方法來載入佈局,不等於null就可以直接利用convertView,因為convertView就是我們之間利用過的View,只不過被移出螢幕後進入到了廢棄快取中,現在又重新拿出來使用而已。然後我們只需要把convertView中的資料更新成當前位置上應該顯示的資料,那麼看起來就好像是全新載入出來的一個佈局一樣,這背後的道理你是不是已經完全搞明白了?
之後的程式碼又都是我們熟悉的流程了,從快取中拿到子View之後再呼叫setupChild()方法將它重新attach到ListView當中,因為快取中的View也是之前從ListView中detach掉的,這部分程式碼就不再重複進行分析了。
為了方便大家理解,這裡我再附上一張圖解說明:
那麼到目前為止,我們就把ListView的整個工作流程程式碼基本分析結束了,文章比較長,希望大家可以理解清楚,下篇文章中會講解我們平時使用ListView時遇到的問題,感興趣的朋友請繼續閱讀 Android ListView非同步載入圖片亂序問題,原因分析及解決方案 。