1. /**
  2. * Unsorted views that can be used by the adapter as a convert view.
  3. */
  4. private ArrayList<View>[] mScrapViews;
  5. private int mViewTypeCount;
  6. private ArrayList<View> mCurrentScrap;
  7. /**
  8. * Fill ActiveViews with all of the children of the AbsListView.
  9. *
  10. * @param childCount
  11. *            The minimum number of views mActiveViews should hold
  12. * @param firstActivePosition
  13. *            The position of the first view that will be stored in
  14. *            mActiveViews
  15. */
  16. void fillActiveViews(int childCount, int firstActivePosition) {
  17. if (mActiveViews.length < childCount) {
  18. mActiveViews = new View[childCount];
  19. }
  20. mFirstActivePosition = firstActivePosition;
  21. final View[] activeViews = mActiveViews;
  22. for (int i = 0; i < childCount; i++) {
  23. View child = getChildAt(i);
  24. AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
  25. // Don't put header or footer views into the scrap heap
  26. if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  27. // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
  28. // active views.
  29. // However, we will NOT place them into scrap views.
  30. activeViews[i] = child;
  31. }
  32. }
  33. }
  34. /**
  35. * Get the view corresponding to the specified position. The view will
  36. * be removed from mActiveViews if it is found.
  37. *
  38. * @param position
  39. *            The position to look up in mActiveViews
  40. * @return The view if it is found, null otherwise
  41. */
  42. View getActiveView(int position) {
  43. int index = position - mFirstActivePosition;
  44. final View[] activeViews = mActiveViews;
  45. if (index >= 0 && index < activeViews.length) {
  46. final View match = activeViews[index];
  47. activeViews[index] = null;
  48. return match;
  49. }
  50. return null;
  51. }
  52. /**
  53. * Put a view into the ScapViews list. These views are unordered.
  54. *
  55. * @param scrap
  56. *            The view to add
  57. */
  58. void addScrapView(View scrap) {
  59. AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
  60. if (lp == null) {
  61. return;
  62. }
  63. // Don't put header or footer views or views that should be ignored
  64. // into the scrap heap
  65. int viewType = lp.viewType;
  66. if (!shouldRecycleViewType(viewType)) {
  67. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  68. removeDetachedView(scrap, false);
  69. }
  70. return;
  71. }
  72. if (mViewTypeCount == 1) {
  73. dispatchFinishTemporaryDetach(scrap);
  74. mCurrentScrap.add(scrap);
  75. } else {
  76. dispatchFinishTemporaryDetach(scrap);
  77. mScrapViews[viewType].add(scrap);
  78. }
  79. if (mRecyclerListener != null) {
  80. mRecyclerListener.onMovedToScrapHeap(scrap);
  81. }
  82. }
  83. /**
  84. * @return A view from the ScrapViews collection. These are unordered.
  85. */
  86. View getScrapView(int position) {
  87. ArrayList<View> scrapViews;
  88. if (mViewTypeCount == 1) {
  89. scrapViews = mCurrentScrap;
  90. int size = scrapViews.size();
  91. if (size > 0) {
  92. return scrapViews.remove(size - 1);
  93. } else {
  94. return null;
  95. }
  96. } else {
  97. int whichScrap = mAdapter.getItemViewType(position);
  98. if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
  99. scrapViews = mScrapViews[whichScrap];
  100. int size = scrapViews.size();
  101. if (size > 0) {
  102. return scrapViews.remove(size - 1);
  103. }
  104. }
  105. }
  106. return null;
  107. }
  108. public void setViewTypeCount(int viewTypeCount) {
  109. if (viewTypeCount < 1) {
  110. throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
  111. }
  112. // noinspection unchecked
  113. ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
  114. for (int i = 0; i < viewTypeCount; i++) {
  115. scrapViews[i] = new ArrayList<View>();
  116. }
  117. mViewTypeCount = viewTypeCount;
  118. mCurrentScrap = scrapViews[0];
  119. mScrapViews = scrapViews;
  120. }
  121. }

这里的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中实现的,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Subclasses should NOT override this method but {@link #layoutChildren()}
  3. * instead.
  4. */
  5. @Override
  6. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  7. super.onLayout(changed, l, t, r, b);
  8. mInLayout = true;
  9. if (changed) {
  10. int childCount = getChildCount();
  11. for (int i = 0; i < childCount; i++) {
  12. getChildAt(i).forceLayout();
  13. }
  14. mRecycler.markChildrenDirty();
  15. }
  16. layoutChildren();
  17. mInLayout = false;
  18. }

可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码如下所示:

[java] view
plain
copy

  1. @Override
  2. protected void layoutChildren() {
  3. final boolean blockLayoutRequests = mBlockLayoutRequests;
  4. if (!blockLayoutRequests) {
  5. mBlockLayoutRequests = true;
  6. } else {
  7. return;
  8. }
  9. try {
  10. super.layoutChildren();
  11. invalidate();
  12. if (mAdapter == null) {
  13. resetList();
  14. invokeOnItemScrollListener();
  15. return;
  16. }
  17. int childrenTop = mListPadding.top;
  18. int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
  19. int childCount = getChildCount();
  20. int index = 0;
  21. int delta = 0;
  22. View sel;
  23. View oldSel = null;
  24. View oldFirst = null;
  25. View newSel = null;
  26. View focusLayoutRestoreView = null;
  27. // Remember stuff we will need down below
  28. switch (mLayoutMode) {
  29. case LAYOUT_SET_SELECTION:
  30. index = mNextSelectedPosition - mFirstPosition;
  31. if (index >= 0 && index < childCount) {
  32. newSel = getChildAt(index);
  33. }
  34. break;
  35. case LAYOUT_FORCE_TOP:
  36. case LAYOUT_FORCE_BOTTOM:
  37. case LAYOUT_SPECIFIC:
  38. case LAYOUT_SYNC:
  39. break;
  40. case LAYOUT_MOVE_SELECTION:
  41. default:
  42. // Remember the previously selected view
  43. index = mSelectedPosition - mFirstPosition;
  44. if (index >= 0 && index < childCount) {
  45. oldSel = getChildAt(index);
  46. }
  47. // Remember the previous first child
  48. oldFirst = getChildAt(0);
  49. if (mNextSelectedPosition >= 0) {
  50. delta = mNextSelectedPosition - mSelectedPosition;
  51. }
  52. // Caution: newSel might be null
  53. newSel = getChildAt(index + delta);
  54. }
  55. boolean dataChanged = mDataChanged;
  56. if (dataChanged) {
  57. handleDataChanged();
  58. }
  59. // Handle the empty set by removing all views that are visible
  60. // and calling it a day
  61. if (mItemCount == 0) {
  62. resetList();
  63. invokeOnItemScrollListener();
  64. return;
  65. } else if (mItemCount != mAdapter.getCount()) {
  66. throw new IllegalStateException("The content of the adapter has changed but "
  67. + "ListView did not receive a notification. Make sure the content of "
  68. + "your adapter is not modified from a background thread, but only "
  69. + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
  70. + ") with Adapter(" + mAdapter.getClass() + ")]");
  71. }
  72. setSelectedPositionInt(mNextSelectedPosition);
  73. // Pull all children into the RecycleBin.
  74. // These views will be reused if possible
  75. final int firstPosition = mFirstPosition;
  76. final RecycleBin recycleBin = mRecycler;
  77. // reset the focus restoration
  78. View focusLayoutRestoreDirectChild = null;
  79. // Don't put header or footer views into the Recycler. Those are
  80. // already cached in mHeaderViews;
  81. if (dataChanged) {
  82. for (int i = 0; i < childCount; i++) {
  83. recycleBin.addScrapView(getChildAt(i));
  84. if (ViewDebug.TRACE_RECYCLER) {
  85. ViewDebug.trace(getChildAt(i),
  86. ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
  87. }
  88. }
  89. } else {
  90. recycleBin.fillActiveViews(childCount, firstPosition);
  91. }
  92. // take focus back to us temporarily to avoid the eventual
  93. // call to clear focus when removing the focused child below
  94. // from messing things up when ViewRoot assigns focus back
  95. // to someone else
  96. final View focusedChild = getFocusedChild();
  97. if (focusedChild != null) {
  98. // TODO: in some cases focusedChild.getParent() == null
  99. // we can remember the focused view to restore after relayout if the
  100. // data hasn't changed, or if the focused position is a header or footer
  101. if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
  102. focusLayoutRestoreDirectChild = focusedChild;
  103. // remember the specific view that had focus
  104. focusLayoutRestoreView = findFocus();
  105. if (focusLayoutRestoreView != null) {
  106. // tell it we are going to mess with it
  107. focusLayoutRestoreView.onStartTemporaryDetach();
  108. }
  109. }
  110. requestFocus();
  111. }
  112. // Clear out old views
  113. detachAllViewsFromParent();
  114. switch (mLayoutMode) {
  115. case LAYOUT_SET_SELECTION:
  116. if (newSel != null) {
  117. sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
  118. } else {
  119. sel = fillFromMiddle(childrenTop, childrenBottom);
  120. }
  121. break;
  122. case LAYOUT_SYNC:
  123. sel = fillSpecific(mSyncPosition, mSpecificTop);
  124. break;
  125. case LAYOUT_FORCE_BOTTOM:
  126. sel = fillUp(mItemCount - 1, childrenBottom);
  127. adjustViewsUpOrDown();
  128. break;
  129. case LAYOUT_FORCE_TOP:
  130. mFirstPosition = 0;
  131. sel = fillFromTop(childrenTop);
  132. adjustViewsUpOrDown();
  133. break;
  134. case LAYOUT_SPECIFIC:
  135. sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
  136. break;
  137. case LAYOUT_MOVE_SELECTION:
  138. sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
  139. break;
  140. default:
  141. if (childCount == 0) {
  142. if (!mStackFromBottom) {
  143. final int position = lookForSelectablePosition(0, true);
  144. setSelectedPositionInt(position);
  145. sel = fillFromTop(childrenTop);
  146. } else {
  147. final int position = lookForSelectablePosition(mItemCount - 1, false);
  148. setSelectedPositionInt(position);
  149. sel = fillUp(mItemCount - 1, childrenBottom);
  150. }
  151. } else {
  152. if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
  153. sel = fillSpecific(mSelectedPosition,
  154. oldSel == null ? childrenTop : oldSel.getTop());
  155. } else if (mFirstPosition < mItemCount) {
  156. sel = fillSpecific(mFirstPosition,
  157. oldFirst == null ? childrenTop : oldFirst.getTop());
  158. } else {
  159. sel = fillSpecific(0, childrenTop);
  160. }
  161. }
  162. break;
  163. }
  164. // Flush any cached views that did not get reused above
  165. recycleBin.scrapActiveViews();
  166. if (sel != null) {
  167. // the current selected item should get focus if items
  168. // are focusable
  169. if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
  170. final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
  171. focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
  172. if (!focusWasTaken) {
  173. // selected item didn't take focus, fine, but still want
  174. // to make sure something else outside of the selected view
  175. // has focus
  176. final View focused = getFocusedChild();
  177. if (focused != null) {
  178. focused.clearFocus();
  179. }
  180. positionSelector(sel);
  181. } else {
  182. sel.setSelected(false);
  183. mSelectorRect.setEmpty();
  184. }
  185. } else {
  186. positionSelector(sel);
  187. }
  188. mSelectedTop = sel.getTop();
  189. } else {
  190. if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
  191. View child = getChildAt(mMotionPosition - mFirstPosition);
  192. if (child != null) positionSelector(child);
  193. } else {
  194. mSelectedTop = 0;
  195. mSelectorRect.setEmpty();
  196. }
  197. // even if there is not selected position, we may need to restore
  198. // focus (i.e. something focusable in touch mode)
  199. if (hasFocus() && focusLayoutRestoreView != null) {
  200. focusLayoutRestoreView.requestFocus();
  201. }
  202. }
  203. // tell focus view we are done mucking with it, if it is still in
  204. // our view hierarchy.
  205. if (focusLayoutRestoreView != null
  206. && focusLayoutRestoreView.getWindowToken() != null) {
  207. focusLayoutRestoreView.onFinishTemporaryDetach();
  208. }
  209. mLayoutMode = LAYOUT_NORMAL;
  210. mDataChanged = false;
  211. mNeedSync = false;
  212. setNextSelectedPositionInt(mSelectedPosition);
  213. updateScrollIndicators();
  214. if (mItemCount > 0) {
  215. checkSelectionChanged();
  216. }
  217. invokeOnItemScrollListener();
  218. } finally {
  219. if (!blockLayoutRequests) {
  220. mBlockLayoutRequests = false;
  221. }
  222. }
  223. }

这段代码比较长,我们挑重点的看。首先可以确定的是,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()方法,我们跟进去瞧一瞧:

[java] view
plain
copy

  1. /**
  2. * Fills the list from top to bottom, starting with mFirstPosition
  3. *
  4. * @param nextTop The location where the top of the first item should be
  5. *        drawn
  6. *
  7. * @return The view that is currently selected
  8. */
  9. private View fillFromTop(int nextTop) {
  10. mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
  11. mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
  12. if (mFirstPosition < 0) {
  13. mFirstPosition = 0;
  14. }
  15. return fillDown(mFirstPosition, nextTop);
  16. }

从这个方法的注释中可以看出,它所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法本身并没有什么逻辑,就是判断了一下mFirstPosition值的合法性,然后调用fillDown()方法,那么我们就有理由可以猜测,填充ListView的操作是在fillDown()方法中完成的。进入fillDown()方法,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Fills the list from pos down to the end of the list view.
  3. *
  4. * @param pos The first position to put in the list
  5. *
  6. * @param nextTop The location where the top of the item associated with pos
  7. *        should be drawn
  8. *
  9. * @return The view that is currently selected, if it happens to be in the
  10. *         range that we draw.
  11. */
  12. private View fillDown(int pos, int nextTop) {
  13. View selectedView = null;
  14. int end = (getBottom() - getTop()) - mListPadding.bottom;
  15. while (nextTop < end && pos < mItemCount) {
  16. // is this the selected item?
  17. boolean selected = pos == mSelectedPosition;
  18. View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
  19. nextTop = child.getBottom() + mDividerHeight;
  20. if (selected) {
  21. selectedView = child;
  22. }
  23. pos++;
  24. }
  25. return selectedView;
  26. }

可以看到,这里使用了一个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()方法,进入到这个方法当中,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Obtain the view and add it to our list of children. The view can be made
  3. * fresh, converted from an unused view, or used as is if it was in the
  4. * recycle bin.
  5. *
  6. * @param position Logical position in the list
  7. * @param y Top or bottom edge of the view to add
  8. * @param flow If flow is true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @return View that was added
  13. */
  14. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  15. boolean selected) {
  16. View child;
  17. if (!mDataChanged) {
  18. // Try to use an exsiting view for this position
  19. child = mRecycler.getActiveView(position);
  20. if (child != null) {
  21. // Found it -- we're using an existing child
  22. // This just needs to be positioned
  23. setupChild(child, position, y, flow, childrenLeft, selected, true);
  24. return child;
  25. }
  26. }
  27. // Make a new view for this position, or convert an unused view if possible
  28. child = obtainView(position, mIsScrap);
  29. // This needs to be positioned and measured
  30. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  31. return child;
  32. }

这里在第19行尝试从RecycleBin当中快速获取一个active view,不过很遗憾的是目前RecycleBin当中还没有缓存任何的View,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行,到第28行会调用obtainView()方法来再次尝试获取一个View,这次的obtainView()方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中。那么obtainView()内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:

[java] view
plain
copy

  1. /**
  2. * Get a view and have it show the data associated with the specified
  3. * position. This is called when we have already discovered that the view is
  4. * not available for reuse in the recycle bin. The only choices left are
  5. * converting an old view or making a new one.
  6. *
  7. * @param position
  8. *            The position to display
  9. * @param isScrap
  10. *            Array of at least 1 boolean, the first entry will become true
  11. *            if the returned view was taken from the scrap heap, false if
  12. *            otherwise.
  13. *
  14. * @return A view displaying the data associated with the specified position
  15. */
  16. View obtainView(int position, boolean[] isScrap) {
  17. isScrap[0] = false;
  18. View scrapView;
  19. scrapView = mRecycler.getScrapView(position);
  20. View child;
  21. if (scrapView != null) {
  22. child = mAdapter.getView(position, scrapView, this);
  23. if (child != scrapView) {
  24. mRecycler.addScrapView(scrapView);
  25. if (mCacheColorHint != 0) {
  26. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  27. }
  28. } else {
  29. isScrap[0] = true;
  30. dispatchFinishTemporaryDetach(child);
  31. }
  32. } else {
  33. child = mAdapter.getView(position, null, this);
  34. if (mCacheColorHint != 0) {
  35. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  36. }
  37. }
  38. return child;
  39. }

obtainView()方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,同样的道理,这里肯定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?当然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。

那么我们平时写ListView的Adapter时,getView()方法通常会怎么写呢?这里我举个简单的例子:

[java] view
plain
copy

  1. @Override
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. Fruit fruit = getItem(position);
  4. View view;
  5. if (convertView == null) {
  6. view = LayoutInflater.from(getContext()).inflate(resourceId, null);
  7. } else {
  8. view = convertView;
  9. }
  10. ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
  11. TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
  12. fruitImage.setImageResource(fruit.getImageId());
  13. fruitName.setText(fruit.getName());
  14. return view;
  15. }

getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。

那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:

[java] view
plain
copy

  1. /**
  2. * Add a view as a child and make sure it is measured (if necessary) and
  3. * positioned properly.
  4. *
  5. * @param child The view to add
  6. * @param position The position of this child
  7. * @param y The y position relative to which this view will be positioned
  8. * @param flowDown If true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @param recycled Has this view been pulled from the recycle bin? If so it
  13. *        does not need to be remeasured.
  14. */
  15. private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
  16. boolean selected, boolean recycled) {
  17. final boolean isSelected = selected && shouldShowSelector();
  18. final boolean updateChildSelected = isSelected != child.isSelected();
  19. final int mode = mTouchMode;
  20. final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
  21. mMotionPosition == position;
  22. final boolean updateChildPressed = isPressed != child.isPressed();
  23. final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
  24. // Respect layout params that are already in the view. Otherwise make some up...
  25. // noinspection unchecked
  26. AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
  27. if (p == null) {
  28. p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
  29. ViewGroup.LayoutParams.WRAP_CONTENT, 0);
  30. }
  31. p.viewType = mAdapter.getItemViewType(position);
  32. if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
  33. p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
  34. attachViewToParent(child, flowDown ? -1 : 0, p);
  35. } else {
  36. p.forceAdd = false;
  37. if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  38. p.recycledHeaderFooter = true;
  39. }
  40. addViewInLayout(child, flowDown ? -1 : 0, p, true);
  41. }
  42. if (updateChildSelected) {
  43. child.setSelected(isSelected);
  44. }
  45. if (updateChildPressed) {
  46. child.setPressed(isPressed);
  47. }
  48. if (needToMeasure) {
  49. int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
  50. mListPadding.left + mListPadding.right, p.width);
  51. int lpHeight = p.height;
  52. int childHeightSpec;
  53. if (lpHeight > 0) {
  54. childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
  55. } else {
  56. childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  57. }
  58. child.measure(childWidthSpec, childHeightSpec);
  59. } else {
  60. cleanupLayoutState(child);
  61. }
  62. final int w = child.getMeasuredWidth();
  63. final int h = child.getMeasuredHeight();
  64. final int childTop = flowDown ? y : y - h;
  65. if (needToMeasure) {
  66. final int childRight = childrenLeft + w;
  67. final int childBottom = childTop + h;
  68. child.layout(childrenLeft, childTop, childRight, childBottom);
  69. } else {
  70. child.offsetLeftAndRight(childrenLeft - child.getLeft());
  71. child.offsetTopAndBottom(childTop - child.getTop());
  72. }
  73. if (mCachingStarted && !child.isDrawingCacheEnabled()) {
  74. child.setDrawingCacheEnabled(true);
  75. }
  76. }

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()方法开始看起:

[java] view
plain
copy

  1. @Override
  2. protected void layoutChildren() {
  3. final boolean blockLayoutRequests = mBlockLayoutRequests;
  4. if (!blockLayoutRequests) {
  5. mBlockLayoutRequests = true;
  6. } else {
  7. return;
  8. }
  9. try {
  10. super.layoutChildren();
  11. invalidate();
  12. if (mAdapter == null) {
  13. resetList();
  14. invokeOnItemScrollListener();
  15. return;
  16. }
  17. int childrenTop = mListPadding.top;
  18. int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
  19. int childCount = getChildCount();
  20. int index = 0;
  21. int delta = 0;
  22. View sel;
  23. View oldSel = null;
  24. View oldFirst = null;
  25. View newSel = null;
  26. View focusLayoutRestoreView = null;
  27. // Remember stuff we will need down below
  28. switch (mLayoutMode) {
  29. case LAYOUT_SET_SELECTION:
  30. index = mNextSelectedPosition - mFirstPosition;
  31. if (index >= 0 && index < childCount) {
  32. newSel = getChildAt(index);
  33. }
  34. break;
  35. case LAYOUT_FORCE_TOP:
  36. case LAYOUT_FORCE_BOTTOM:
  37. case LAYOUT_SPECIFIC:
  38. case LAYOUT_SYNC:
  39. break;
  40. case LAYOUT_MOVE_SELECTION:
  41. default:
  42. // Remember the previously selected view
  43. index = mSelectedPosition - mFirstPosition;
  44. if (index >= 0 && index < childCount) {
  45. oldSel = getChildAt(index);
  46. }
  47. // Remember the previous first child
  48. oldFirst = getChildAt(0);
  49. if (mNextSelectedPosition >= 0) {
  50. delta = mNextSelectedPosition - mSelectedPosition;
  51. }
  52. // Caution: newSel might be null
  53. newSel = getChildAt(index + delta);
  54. }
  55. boolean dataChanged = mDataChanged;
  56. if (dataChanged) {
  57. handleDataChanged();
  58. }
  59. // Handle the empty set by removing all views that are visible
  60. // and calling it a day
  61. if (mItemCount == 0) {
  62. resetList();
  63. invokeOnItemScrollListener();
  64. return;
  65. } else if (mItemCount != mAdapter.getCount()) {
  66. throw new IllegalStateException("The content of the adapter has changed but "
  67. + "ListView did not receive a notification. Make sure the content of "
  68. + "your adapter is not modified from a background thread, but only "
  69. + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
  70. + ") with Adapter(" + mAdapter.getClass() + ")]");
  71. }
  72. setSelectedPositionInt(mNextSelectedPosition);
  73. // Pull all children into the RecycleBin.
  74. // These views will be reused if possible
  75. final int firstPosition = mFirstPosition;
  76. final RecycleBin recycleBin = mRecycler;
  77. // reset the focus restoration
  78. View focusLayoutRestoreDirectChild = null;
  79. // Don't put header or footer views into the Recycler. Those are
  80. // already cached in mHeaderViews;
  81. if (dataChanged) {
  82. for (int i = 0; i < childCount; i++) {
  83. recycleBin.addScrapView(getChildAt(i));
  84. if (ViewDebug.TRACE_RECYCLER) {
  85. ViewDebug.trace(getChildAt(i),
  86. ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
  87. }
  88. }
  89. } else {
  90. recycleBin.fillActiveViews(childCount, firstPosition);
  91. }
  92. // take focus back to us temporarily to avoid the eventual
  93. // call to clear focus when removing the focused child below
  94. // from messing things up when ViewRoot assigns focus back
  95. // to someone else
  96. final View focusedChild = getFocusedChild();
  97. if (focusedChild != null) {
  98. // TODO: in some cases focusedChild.getParent() == null
  99. // we can remember the focused view to restore after relayout if the
  100. // data hasn't changed, or if the focused position is a header or footer
  101. if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
  102. focusLayoutRestoreDirectChild = focusedChild;
  103. // remember the specific view that had focus
  104. focusLayoutRestoreView = findFocus();
  105. if (focusLayoutRestoreView != null) {
  106. // tell it we are going to mess with it
  107. focusLayoutRestoreView.onStartTemporaryDetach();
  108. }
  109. }
  110. requestFocus();
  111. }
  112. // Clear out old views
  113. detachAllViewsFromParent();
  114. switch (mLayoutMode) {
  115. case LAYOUT_SET_SELECTION:
  116. if (newSel != null) {
  117. sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
  118. } else {
  119. sel = fillFromMiddle(childrenTop, childrenBottom);
  120. }
  121. break;
  122. case LAYOUT_SYNC:
  123. sel = fillSpecific(mSyncPosition, mSpecificTop);
  124. break;
  125. case LAYOUT_FORCE_BOTTOM:
  126. sel = fillUp(mItemCount - 1, childrenBottom);
  127. adjustViewsUpOrDown();
  128. break;
  129. case LAYOUT_FORCE_TOP:
  130. mFirstPosition = 0;
  131. sel = fillFromTop(childrenTop);
  132. adjustViewsUpOrDown();
  133. break;
  134. case LAYOUT_SPECIFIC:
  135. sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
  136. break;
  137. case LAYOUT_MOVE_SELECTION:
  138. sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
  139. break;
  140. default:
  141. if (childCount == 0) {
  142. if (!mStackFromBottom) {
  143. final int position = lookForSelectablePosition(0, true);
  144. setSelectedPositionInt(position);
  145. sel = fillFromTop(childrenTop);
  146. } else {
  147. final int position = lookForSelectablePosition(mItemCount - 1, false);
  148. setSelectedPositionInt(position);
  149. sel = fillUp(mItemCount - 1, childrenBottom);
  150. }
  151. } else {
  152. if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
  153. sel = fillSpecific(mSelectedPosition,
  154. oldSel == null ? childrenTop : oldSel.getTop());
  155. } else if (mFirstPosition < mItemCount) {
  156. sel = fillSpecific(mFirstPosition,
  157. oldFirst == null ? childrenTop : oldFirst.getTop());
  158. } else {
  159. sel = fillSpecific(0, childrenTop);
  160. }
  161. }
  162. break;
  163. }
  164. // Flush any cached views that did not get reused above
  165. recycleBin.scrapActiveViews();
  166. if (sel != null) {
  167. // the current selected item should get focus if items
  168. // are focusable
  169. if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
  170. final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
  171. focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
  172. if (!focusWasTaken) {
  173. // selected item didn't take focus, fine, but still want
  174. // to make sure something else outside of the selected view
  175. // has focus
  176. final View focused = getFocusedChild();
  177. if (focused != null) {
  178. focused.clearFocus();
  179. }
  180. positionSelector(sel);
  181. } else {
  182. sel.setSelected(false);
  183. mSelectorRect.setEmpty();
  184. }
  185. } else {
  186. positionSelector(sel);
  187. }
  188. mSelectedTop = sel.getTop();
  189. } else {
  190. if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
  191. View child = getChildAt(mMotionPosition - mFirstPosition);
  192. if (child != null) positionSelector(child);
  193. } else {
  194. mSelectedTop = 0;
  195. mSelectorRect.setEmpty();
  196. }
  197. // even if there is not selected position, we may need to restore
  198. // focus (i.e. something focusable in touch mode)
  199. if (hasFocus() && focusLayoutRestoreView != null) {
  200. focusLayoutRestoreView.requestFocus();
  201. }
  202. }
  203. // tell focus view we are done mucking with it, if it is still in
  204. // our view hierarchy.
  205. if (focusLayoutRestoreView != null
  206. && focusLayoutRestoreView.getWindowToken() != null) {
  207. focusLayoutRestoreView.onFinishTemporaryDetach();
  208. }
  209. mLayoutMode = LAYOUT_NORMAL;
  210. mDataChanged = false;
  211. mNeedSync = false;
  212. setNextSelectedPositionInt(mSelectedPosition);
  213. updateScrollIndicators();
  214. if (mItemCount > 0) {
  215. checkSelectionChanged();
  216. }
  217. invokeOnItemScrollListener();
  218. } finally {
  219. if (!blockLayoutRequests) {
  220. mBlockLayoutRequests = false;
  221. }
  222. }
  223. }

同样还是在第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()方法当中,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Put a specific item at a specific location on the screen and then build
  3. * up and down from there.
  4. *
  5. * @param position The reference view to use as the starting point
  6. * @param top Pixel offset from the top of this view to the top of the
  7. *        reference view.
  8. *
  9. * @return The selected view, or null if the selected view is outside the
  10. *         visible area.
  11. */
  12. private View fillSpecific(int position, int top) {
  13. boolean tempIsSelected = position == mSelectedPosition;
  14. View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
  15. // Possibly changed again in fillUp if we add rows above this one.
  16. mFirstPosition = position;
  17. View above;
  18. View below;
  19. final int dividerHeight = mDividerHeight;
  20. if (!mStackFromBottom) {
  21. above = fillUp(position - 1, temp.getTop() - dividerHeight);
  22. // This will correct for the top of the first view not touching the top of the list
  23. adjustViewsUpOrDown();
  24. below = fillDown(position + 1, temp.getBottom() + dividerHeight);
  25. int childCount = getChildCount();
  26. if (childCount > 0) {
  27. correctTooHigh(childCount);
  28. }
  29. } else {
  30. below = fillDown(position + 1, temp.getBottom() + dividerHeight);
  31. // This will correct for the bottom of the last view not touching the bottom of the list
  32. adjustViewsUpOrDown();
  33. above = fillUp(position - 1, temp.getTop() - dividerHeight);
  34. int childCount = getChildCount();
  35. if (childCount > 0) {
  36. correctTooLow(childCount);
  37. }
  38. }
  39. if (tempIsSelected) {
  40. return temp;
  41. } else if (above != null) {
  42. return above;
  43. } else {
  44. return below;
  45. }
  46. }

fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Obtain the view and add it to our list of children. The view can be made
  3. * fresh, converted from an unused view, or used as is if it was in the
  4. * recycle bin.
  5. *
  6. * @param position Logical position in the list
  7. * @param y Top or bottom edge of the view to add
  8. * @param flow If flow is true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @return View that was added
  13. */
  14. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  15. boolean selected) {
  16. View child;
  17. if (!mDataChanged) {
  18. // Try to use an exsiting view for this position
  19. child = mRecycler.getActiveView(position);
  20. if (child != null) {
  21. // Found it -- we're using an existing child
  22. // This just needs to be positioned
  23. setupChild(child, position, y, flow, childrenLeft, selected, true);
  24. return child;
  25. }
  26. }
  27. // Make a new view for this position, or convert an unused view if possible
  28. child = obtainView(position, mIsScrap);
  29. // This needs to be positioned and measured
  30. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  31. return child;
  32. }

仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。

注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:

[java] view
plain
copy

  1. /**
  2. * Add a view as a child and make sure it is measured (if necessary) and
  3. * positioned properly.
  4. *
  5. * @param child The view to add
  6. * @param position The position of this child
  7. * @param y The y position relative to which this view will be positioned
  8. * @param flowDown If true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @param recycled Has this view been pulled from the recycle bin? If so it
  13. *        does not need to be remeasured.
  14. */
  15. private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
  16. boolean selected, boolean recycled) {
  17. final boolean isSelected = selected && shouldShowSelector();
  18. final boolean updateChildSelected = isSelected != child.isSelected();
  19. final int mode = mTouchMode;
  20. final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
  21. mMotionPosition == position;
  22. final boolean updateChildPressed = isPressed != child.isPressed();
  23. final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
  24. // Respect layout params that are already in the view. Otherwise make some up...
  25. // noinspection unchecked
  26. AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
  27. if (p == null) {
  28. p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
  29. ViewGroup.LayoutParams.WRAP_CONTENT, 0);
  30. }
  31. p.viewType = mAdapter.getItemViewType(position);
  32. if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
  33. p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
  34. attachViewToParent(child, flowDown ? -1 : 0, p);
  35. } else {
  36. p.forceAdd = false;
  37. if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  38. p.recycledHeaderFooter = true;
  39. }
  40. addViewInLayout(child, flowDown ? -1 : 0, p, true);
  41. }
  42. if (updateChildSelected) {
  43. child.setSelected(isSelected);
  44. }
  45. if (updateChildPressed) {
  46. child.setPressed(isPressed);
  47. }
  48. if (needToMeasure) {
  49. int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
  50. mListPadding.left + mListPadding.right, p.width);
  51. int lpHeight = p.height;
  52. int childHeightSpec;
  53. if (lpHeight > 0) {
  54. childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
  55. } else {
  56. childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  57. }
  58. child.measure(childWidthSpec, childHeightSpec);
  59. } else {
  60. cleanupLayoutState(child);
  61. }
  62. final int w = child.getMeasuredWidth();
  63. final int h = child.getMeasuredHeight();
  64. final int childTop = flowDown ? y : y - h;
  65. if (needToMeasure) {
  66. final int childRight = childrenLeft + w;
  67. final int childBottom = childTop + h;
  68. child.layout(childrenLeft, childTop, childRight, childBottom);
  69. } else {
  70. child.offsetLeftAndRight(childrenLeft - child.getLeft());
  71. child.offsetTopAndBottom(childTop - child.getTop());
  72. }
  73. if (mCachingStarted && !child.isDrawingCacheEnabled()) {
  74. child.setDrawingCacheEnabled(true);
  75. }
  76. }

可以看到,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中的这个方法:

[java] view
plain
copy

  1. @Override
  2. public boolean onTouchEvent(MotionEvent ev) {
  3. if (!isEnabled()) {
  4. // A disabled view that is clickable still consumes the touch
  5. // events, it just doesn't respond to them.
  6. return isClickable() || isLongClickable();
  7. }
  8. final int action = ev.getAction();
  9. View v;
  10. int deltaY;
  11. if (mVelocityTracker == null) {
  12. mVelocityTracker = VelocityTracker.obtain();
  13. }
  14. mVelocityTracker.addMovement(ev);
  15. switch (action & MotionEvent.ACTION_MASK) {
  16. case MotionEvent.ACTION_DOWN: {
  17. mActivePointerId = ev.getPointerId(0);
  18. final int x = (int) ev.getX();
  19. final int y = (int) ev.getY();
  20. int motionPosition = pointToPosition(x, y);
  21. if (!mDataChanged) {
  22. if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
  23. && (getAdapter().isEnabled(motionPosition))) {
  24. // User clicked on an actual view (and was not stopping a
  25. // fling). It might be a
  26. // click or a scroll. Assume it is a click until proven
  27. // otherwise
  28. mTouchMode = TOUCH_MODE_DOWN;
  29. // FIXME Debounce
  30. if (mPendingCheckForTap == null) {
  31. mPendingCheckForTap = new CheckForTap();
  32. }
  33. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  34. } else {
  35. if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
  36. // If we couldn't find a view to click on, but the down
  37. // event was touching
  38. // the edge, we will bail out and try again. This allows
  39. // the edge correcting
  40. // code in ViewRoot to try to find a nearby view to
  41. // select
  42. return false;
  43. }
  44. if (mTouchMode == TOUCH_MODE_FLING) {
  45. // Stopped a fling. It is a scroll.
  46. createScrollingCache();
  47. mTouchMode = TOUCH_MODE_SCROLL;
  48. mMotionCorrection = 0;
  49. motionPosition = findMotionRow(y);
  50. reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
  51. }
  52. }
  53. }
  54. if (motionPosition >= 0) {
  55. // Remember where the motion event started
  56. v = getChildAt(motionPosition - mFirstPosition);
  57. mMotionViewOriginalTop = v.getTop();
  58. }
  59. mMotionX = x;
  60. mMotionY = y;
  61. mMotionPosition = motionPosition;
  62. mLastY = Integer.MIN_VALUE;
  63. break;
  64. }
  65. case MotionEvent.ACTION_MOVE: {
  66. final int pointerIndex = ev.findPointerIndex(mActivePointerId);
  67. final int y = (int) ev.getY(pointerIndex);
  68. deltaY = y - mMotionY;
  69. switch (mTouchMode) {
  70. case TOUCH_MODE_DOWN:
  71. case TOUCH_MODE_TAP:
  72. case TOUCH_MODE_DONE_WAITING:
  73. // Check if we have moved far enough that it looks more like a
  74. // scroll than a tap
  75. startScrollIfNeeded(deltaY);
  76. break;
  77. case TOUCH_MODE_SCROLL:
  78. if (PROFILE_SCROLLING) {
  79. if (!mScrollProfilingStarted) {
  80. Debug.startMethodTracing("AbsListViewScroll");
  81. mScrollProfilingStarted = true;
  82. }
  83. }
  84. if (y != mLastY) {
  85. deltaY -= mMotionCorrection;
  86. int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
  87. // No need to do all this work if we're not going to move
  88. // anyway
  89. boolean atEdge = false;
  90. if (incrementalDeltaY != 0) {
  91. atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
  92. }
  93. // Check to see if we have bumped into the scroll limit
  94. if (atEdge && getChildCount() > 0) {
  95. // Treat this like we're starting a new scroll from the
  96. // current
  97. // position. This will let the user start scrolling back
  98. // into
  99. // content immediately rather than needing to scroll
  100. // back to the
  101. // point where they hit the limit first.
  102. int motionPosition = findMotionRow(y);
  103. if (motionPosition >= 0) {
  104. final View motionView = getChildAt(motionPosition - mFirstPosition);
  105. mMotionViewOriginalTop = motionView.getTop();
  106. }
  107. mMotionY = y;
  108. mMotionPosition = motionPosition;
  109. invalidate();
  110. }
  111. mLastY = y;
  112. }
  113. break;
  114. }
  115. break;
  116. }
  117. case MotionEvent.ACTION_UP: {
  118. switch (mTouchMode) {
  119. case TOUCH_MODE_DOWN:
  120. case TOUCH_MODE_TAP:
  121. case TOUCH_MODE_DONE_WAITING:
  122. final int motionPosition = mMotionPosition;
  123. final View child = getChildAt(motionPosition - mFirstPosition);
  124. if (child != null && !child.hasFocusable()) {
  125. if (mTouchMode != TOUCH_MODE_DOWN) {
  126. child.setPressed(false);
  127. }
  128. if (mPerformClick == null) {
  129. mPerformClick = new PerformClick();
  130. }
  131. final AbsListView.PerformClick performClick = mPerformClick;
  132. performClick.mChild = child;
  133. performClick.mClickMotionPosition = motionPosition;
  134. performClick.rememberWindowAttachCount();
  135. mResurrectToPosition = motionPosition;
  136. if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
  137. final Handler handler = getHandler();
  138. if (handler != null) {
  139. handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
  140. : mPendingCheckForLongPress);
  141. }
  142. mLayoutMode = LAYOUT_NORMAL;
  143. if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  144. mTouchMode = TOUCH_MODE_TAP;
  145. setSelectedPositionInt(mMotionPosition);
  146. layoutChildren();
  147. child.setPressed(true);
  148. positionSelector(child);
  149. setPressed(true);
  150. if (mSelector != null) {
  151. Drawable d = mSelector.getCurrent();
  152. if (d != null && d instanceof TransitionDrawable) {
  153. ((TransitionDrawable) d).resetTransition();
  154. }
  155. }
  156. postDelayed(new Runnable() {
  157. public void run() {
  158. child.setPressed(false);
  159. setPressed(false);
  160. if (!mDataChanged) {
  161. post(performClick);
  162. }
  163. mTouchMode = TOUCH_MODE_REST;
  164. }
  165. }, ViewConfiguration.getPressedStateDuration());
  166. } else {
  167. mTouchMode = TOUCH_MODE_REST;
  168. }
  169. return true;
  170. } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  171. post(performClick);
  172. }
  173. }
  174. mTouchMode = TOUCH_MODE_REST;
  175. break;
  176. case TOUCH_MODE_SCROLL:
  177. final int childCount = getChildCount();
  178. if (childCount > 0) {
  179. if (mFirstPosition == 0
  180. && getChildAt(0).getTop() >= mListPadding.top
  181. && mFirstPosition + childCount < mItemCount
  182. && getChildAt(childCount - 1).getBottom() <= getHeight()
  183. - mListPadding.bottom) {
  184. mTouchMode = TOUCH_MODE_REST;
  185. reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  186. } else {
  187. final VelocityTracker velocityTracker = mVelocityTracker;
  188. velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  189. final int initialVelocity = (int) velocityTracker
  190. .getYVelocity(mActivePointerId);
  191. if (Math.abs(initialVelocity) > mMinimumVelocity) {
  192. if (mFlingRunnable == null) {
  193. mFlingRunnable = new FlingRunnable();
  194. }
  195. reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
  196. mFlingRunnable.start(-initialVelocity);
  197. } else {
  198. mTouchMode = TOUCH_MODE_REST;
  199. reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  200. }
  201. }
  202. } else {
  203. mTouchMode = TOUCH_MODE_REST;
  204. reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  205. }
  206. break;
  207. }
  208. setPressed(false);
  209. // Need to redraw since we probably aren't drawing the selector
  210. // anymore
  211. invalidate();
  212. final Handler handler = getHandler();
  213. if (handler != null) {
  214. handler.removeCallbacks(mPendingCheckForLongPress);
  215. }
  216. if (mVelocityTracker != null) {
  217. mVelocityTracker.recycle();
  218. mVelocityTracker = null;
  219. }
  220. mActivePointerId = INVALID_POINTER;
  221. if (PROFILE_SCROLLING) {
  222. if (mScrollProfilingStarted) {
  223. Debug.stopMethodTracing();
  224. mScrollProfilingStarted = false;
  225. }
  226. }
  227. break;
  228. }
  229. case MotionEvent.ACTION_CANCEL: {
  230. mTouchMode = TOUCH_MODE_REST;
  231. setPressed(false);
  232. View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
  233. if (motionView != null) {
  234. motionView.setPressed(false);
  235. }
  236. clearScrollingCache();
  237. final Handler handler = getHandler();
  238. if (handler != null) {
  239. handler.removeCallbacks(mPendingCheckForLongPress);
  240. }
  241. if (mVelocityTracker != null) {
  242. mVelocityTracker.recycle();
  243. mVelocityTracker = null;
  244. }
  245. mActivePointerId = INVALID_POINTER;
  246. break;
  247. }
  248. case MotionEvent.ACTION_POINTER_UP: {
  249. onSecondaryPointerUp(ev);
  250. final int x = mMotionX;
  251. final int y = mMotionY;
  252. final int motionPosition = pointToPosition(x, y);
  253. if (motionPosition >= 0) {
  254. // Remember where the motion event started
  255. v = getChildAt(motionPosition - mFirstPosition);
  256. mMotionViewOriginalTop = v.getTop();
  257. mMotionPosition = motionPosition;
  258. }
  259. mLastY = y;
  260. break;
  261. }
  262. }
  263. return true;
  264. }

这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,那么我们就只看这部分代码就可以了。

可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解了,喜欢寻根究底的朋友们可以自己去源码里找一找原因。

这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并没有什么太多需要注意的东西,唯一一点非常重要的就是第92行调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。那么我们进入到这个方法中瞧一瞧,代码如下所示:

[java] view
plain
copy

  1. boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
  2. final int childCount = getChildCount();
  3. if (childCount == 0) {
  4. return true;
  5. }
  6. final int firstTop = getChildAt(0).getTop();
  7. final int lastBottom = getChildAt(childCount - 1).getBottom();
  8. final Rect listPadding = mListPadding;
  9. final int spaceAbove = listPadding.top - firstTop;
  10. final int end = getHeight() - listPadding.bottom;
  11. final int spaceBelow = lastBottom - end;
  12. final int height = getHeight() - getPaddingBottom() - getPaddingTop();
  13. if (deltaY < 0) {
  14. deltaY = Math.max(-(height - 1), deltaY);
  15. } else {
  16. deltaY = Math.min(height - 1, deltaY);
  17. }
  18. if (incrementalDeltaY < 0) {
  19. incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
  20. } else {
  21. incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
  22. }
  23. final int firstPosition = mFirstPosition;
  24. if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
  25. // Don't need to move views down if the top of the first position
  26. // is already visible
  27. return true;
  28. }
  29. if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
  30. // Don't need to move views up if the bottom of the last position
  31. // is already visible
  32. return true;
  33. }
  34. final boolean down = incrementalDeltaY < 0;
  35. final boolean inTouchMode = isInTouchMode();
  36. if (inTouchMode) {
  37. hideSelector();
  38. }
  39. final int headerViewsCount = getHeaderViewsCount();
  40. final int footerViewsStart = mItemCount - getFooterViewsCount();
  41. int start = 0;
  42. int count = 0;
  43. if (down) {
  44. final int top = listPadding.top - incrementalDeltaY;
  45. for (int i = 0; i < childCount; i++) {
  46. final View child = getChildAt(i);
  47. if (child.getBottom() >= top) {
  48. break;
  49. } else {
  50. count++;
  51. int position = firstPosition + i;
  52. if (position >= headerViewsCount && position < footerViewsStart) {
  53. mRecycler.addScrapView(child);
  54. }
  55. }
  56. }
  57. } else {
  58. final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
  59. for (int i = childCount - 1; i >= 0; i--) {
  60. final View child = getChildAt(i);
  61. if (child.getTop() <= bottom) {
  62. break;
  63. } else {
  64. start = i;
  65. count++;
  66. int position = firstPosition + i;
  67. if (position >= headerViewsCount && position < footerViewsStart) {
  68. mRecycler.addScrapView(child);
  69. }
  70. }
  71. }
  72. }
  73. mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
  74. mBlockLayoutRequests = true;
  75. if (count > 0) {
  76. detachViewsFromParent(start, count);
  77. }
  78. offsetChildrenTopAndBottom(incrementalDeltaY);
  79. if (down) {
  80. mFirstPosition += count;
  81. }
  82. invalidate();
  83. final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
  84. if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
  85. fillGap(down);
  86. }
  87. if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
  88. final int childIndex = mSelectedPosition - mFirstPosition;
  89. if (childIndex >= 0 && childIndex < getChildCount()) {
  90. positionSelector(getChildAt(childIndex));
  91. }
  92. }
  93. mBlockLayoutRequests = false;
  94. invokeOnItemScrollListener();
  95. awakenScrollBars();
  96. return false;
  97. }

这个方法接收两个参数,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()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,如下所示:

[java] view
plain
copy

  1. /**
  2. * Fills the gap left open by a touch-scroll. During a touch scroll,
  3. * children that remain on screen are shifted and the other ones are
  4. * discarded. The role of this method is to fill the gap thus created by
  5. * performing a partial layout in the empty space.
  6. *
  7. * @param down
  8. *            true if the scroll is going down, false if it is going up
  9. */
  10. abstract void fillGap(boolean down);

OK,AbsListView中的fillGap()是一个抽象方法,那么我们立刻就能够想到,它的具体实现肯定是在ListView中完成的了。回到ListView当中,fillGap()方法的代码如下所示:

[java] view
plain
copy

  1. void fillGap(boolean down) {
  2. final int count = getChildCount();
  3. if (down) {
  4. final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
  5. getListPaddingTop();
  6. fillDown(mFirstPosition + count, startOffset);
  7. correctTooHigh(getChildCount());
  8. } else {
  9. final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
  10. getHeight() - getListPaddingBottom();
  11. fillUp(mFirstPosition - 1, startOffset);
  12. correctTooLow(getChildCount());
  13. }
  14. }

down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:

[java] view
plain
copy

  1. /**
  2. * Obtain the view and add it to our list of children. The view can be made
  3. * fresh, converted from an unused view, or used as is if it was in the
  4. * recycle bin.
  5. *
  6. * @param position Logical position in the list
  7. * @param y Top or bottom edge of the view to add
  8. * @param flow If flow is true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @return View that was added
  13. */
  14. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  15. boolean selected) {
  16. View child;
  17. if (!mDataChanged) {
  18. // Try to use an exsiting view for this position
  19. child = mRecycler.getActiveView(position);
  20. if (child != null) {
  21. // Found it -- we're using an existing child
  22. // This just needs to be positioned
  23. setupChild(child, position, y, flow, childrenLeft, selected, true);
  24. return child;
  25. }
  26. }
  27. // Make a new view for this position, or convert an unused view if possible
  28. child = obtainView(position, mIsScrap);
  29. // This needs to be positioned and measured
  30. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  31. return child;
  32. }

不管怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。

既然getActiveView()方法返回的值是null,那么就还是会走到第28行的obtainView()方法当中,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Get a view and have it show the data associated with the specified
  3. * position. This is called when we have already discovered that the view is
  4. * not available for reuse in the recycle bin. The only choices left are
  5. * converting an old view or making a new one.
  6. *
  7. * @param position
  8. *            The position to display
  9. * @param isScrap
  10. *            Array of at least 1 boolean, the first entry will become true
  11. *            if the returned view was taken from the scrap heap, false if
  12. *            otherwise.
  13. *
  14. * @return A view displaying the data associated with the specified position
  15. */
  16. View obtainView(int position, boolean[] isScrap) {
  17. isScrap[0] = false;
  18. View scrapView;
  19. scrapView = mRecycler.getScrapView(position);
  20. View child;
  21. if (scrapView != null) {
  22. child = mAdapter.getView(position, scrapView, this);
  23. if (child != scrapView) {
  24. mRecycler.addScrapView(scrapView);
  25. if (mCacheColorHint != 0) {
  26. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  27. }
  28. } else {
  29. isScrap[0] = true;
  30. dispatchFinishTemporaryDetach(child);
  31. }
  32. } else {
  33. child = mAdapter.getView(position, null, this);
  34. if (mCacheColorHint != 0) {
  35. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  36. }
  37. }
  38. return child;
  39. }

这里在第19行会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,然后我们在第22行将它作为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getView()方法示例:

[java] view
plain
copy

  1. @Override
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. Fruit fruit = getItem(position);
  4. View view;
  5. if (convertView == null) {
  6. view = LayoutInflater.from(getContext()).inflate(resourceId, null);
  7. } else {
  8. view = convertView;
  9. }
  10. ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
  11. TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
  12. fruitImage.setImageResource(fruit.getImageId());
  13. fruitName.setText(fruit.getName());
  14. return view;
  15. }

第二个参数就是我们最熟悉的convertView呀,难怪平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?

之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。

为了方便大家理解,这里我再附上一张图解说明:

那么到目前为止,我们就把ListView的整个工作流程代码基本分析结束了,文章比较长,希望大家可以理解清楚,下篇文章中会讲解我们平时使用ListView时遇到的问题,敬请期待。

  1. /**
  2. * Unsorted views that can be used by the adapter as a convert view.
  3. */
  4. private ArrayList<View>[] mScrapViews;
  5. private int mViewTypeCount;
  6. private ArrayList<View> mCurrentScrap;
  7. /**
  8. * Fill ActiveViews with all of the children of the AbsListView.
  9. *
  10. * @param childCount
  11. *            The minimum number of views mActiveViews should hold
  12. * @param firstActivePosition
  13. *            The position of the first view that will be stored in
  14. *            mActiveViews
  15. */
  16. void fillActiveViews(int childCount, int firstActivePosition) {
  17. if (mActiveViews.length < childCount) {
  18. mActiveViews = new View[childCount];
  19. }
  20. mFirstActivePosition = firstActivePosition;
  21. final View[] activeViews = mActiveViews;
  22. for (int i = 0; i < childCount; i++) {
  23. View child = getChildAt(i);
  24. AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
  25. // Don't put header or footer views into the scrap heap
  26. if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  27. // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
  28. // active views.
  29. // However, we will NOT place them into scrap views.
  30. activeViews[i] = child;
  31. }
  32. }
  33. }
  34. /**
  35. * Get the view corresponding to the specified position. The view will
  36. * be removed from mActiveViews if it is found.
  37. *
  38. * @param position
  39. *            The position to look up in mActiveViews
  40. * @return The view if it is found, null otherwise
  41. */
  42. View getActiveView(int position) {
  43. int index = position - mFirstActivePosition;
  44. final View[] activeViews = mActiveViews;
  45. if (index >= 0 && index < activeViews.length) {
  46. final View match = activeViews[index];
  47. activeViews[index] = null;
  48. return match;
  49. }
  50. return null;
  51. }
  52. /**
  53. * Put a view into the ScapViews list. These views are unordered.
  54. *
  55. * @param scrap
  56. *            The view to add
  57. */
  58. void addScrapView(View scrap) {
  59. AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
  60. if (lp == null) {
  61. return;
  62. }
  63. // Don't put header or footer views or views that should be ignored
  64. // into the scrap heap
  65. int viewType = lp.viewType;
  66. if (!shouldRecycleViewType(viewType)) {
  67. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  68. removeDetachedView(scrap, false);
  69. }
  70. return;
  71. }
  72. if (mViewTypeCount == 1) {
  73. dispatchFinishTemporaryDetach(scrap);
  74. mCurrentScrap.add(scrap);
  75. } else {
  76. dispatchFinishTemporaryDetach(scrap);
  77. mScrapViews[viewType].add(scrap);
  78. }
  79. if (mRecyclerListener != null) {
  80. mRecyclerListener.onMovedToScrapHeap(scrap);
  81. }
  82. }
  83. /**
  84. * @return A view from the ScrapViews collection. These are unordered.
  85. */
  86. View getScrapView(int position) {
  87. ArrayList<View> scrapViews;
  88. if (mViewTypeCount == 1) {
  89. scrapViews = mCurrentScrap;
  90. int size = scrapViews.size();
  91. if (size > 0) {
  92. return scrapViews.remove(size - 1);
  93. } else {
  94. return null;
  95. }
  96. } else {
  97. int whichScrap = mAdapter.getItemViewType(position);
  98. if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
  99. scrapViews = mScrapViews[whichScrap];
  100. int size = scrapViews.size();
  101. if (size > 0) {
  102. return scrapViews.remove(size - 1);
  103. }
  104. }
  105. }
  106. return null;
  107. }
  108. public void setViewTypeCount(int viewTypeCount) {
  109. if (viewTypeCount < 1) {
  110. throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
  111. }
  112. // noinspection unchecked
  113. ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
  114. for (int i = 0; i < viewTypeCount; i++) {
  115. scrapViews[i] = new ArrayList<View>();
  116. }
  117. mViewTypeCount = viewTypeCount;
  118. mCurrentScrap = scrapViews[0];
  119. mScrapViews = scrapViews;
  120. }
  121. }

这里的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中实现的,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Subclasses should NOT override this method but {@link #layoutChildren()}
  3. * instead.
  4. */
  5. @Override
  6. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  7. super.onLayout(changed, l, t, r, b);
  8. mInLayout = true;
  9. if (changed) {
  10. int childCount = getChildCount();
  11. for (int i = 0; i < childCount; i++) {
  12. getChildAt(i).forceLayout();
  13. }
  14. mRecycler.markChildrenDirty();
  15. }
  16. layoutChildren();
  17. mInLayout = false;
  18. }

可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码如下所示:

[java] view
plain
copy

  1. @Override
  2. protected void layoutChildren() {
  3. final boolean blockLayoutRequests = mBlockLayoutRequests;
  4. if (!blockLayoutRequests) {
  5. mBlockLayoutRequests = true;
  6. } else {
  7. return;
  8. }
  9. try {
  10. super.layoutChildren();
  11. invalidate();
  12. if (mAdapter == null) {
  13. resetList();
  14. invokeOnItemScrollListener();
  15. return;
  16. }
  17. int childrenTop = mListPadding.top;
  18. int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
  19. int childCount = getChildCount();
  20. int index = 0;
  21. int delta = 0;
  22. View sel;
  23. View oldSel = null;
  24. View oldFirst = null;
  25. View newSel = null;
  26. View focusLayoutRestoreView = null;
  27. // Remember stuff we will need down below
  28. switch (mLayoutMode) {
  29. case LAYOUT_SET_SELECTION:
  30. index = mNextSelectedPosition - mFirstPosition;
  31. if (index >= 0 && index < childCount) {
  32. newSel = getChildAt(index);
  33. }
  34. break;
  35. case LAYOUT_FORCE_TOP:
  36. case LAYOUT_FORCE_BOTTOM:
  37. case LAYOUT_SPECIFIC:
  38. case LAYOUT_SYNC:
  39. break;
  40. case LAYOUT_MOVE_SELECTION:
  41. default:
  42. // Remember the previously selected view
  43. index = mSelectedPosition - mFirstPosition;
  44. if (index >= 0 && index < childCount) {
  45. oldSel = getChildAt(index);
  46. }
  47. // Remember the previous first child
  48. oldFirst = getChildAt(0);
  49. if (mNextSelectedPosition >= 0) {
  50. delta = mNextSelectedPosition - mSelectedPosition;
  51. }
  52. // Caution: newSel might be null
  53. newSel = getChildAt(index + delta);
  54. }
  55. boolean dataChanged = mDataChanged;
  56. if (dataChanged) {
  57. handleDataChanged();
  58. }
  59. // Handle the empty set by removing all views that are visible
  60. // and calling it a day
  61. if (mItemCount == 0) {
  62. resetList();
  63. invokeOnItemScrollListener();
  64. return;
  65. } else if (mItemCount != mAdapter.getCount()) {
  66. throw new IllegalStateException("The content of the adapter has changed but "
  67. + "ListView did not receive a notification. Make sure the content of "
  68. + "your adapter is not modified from a background thread, but only "
  69. + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
  70. + ") with Adapter(" + mAdapter.getClass() + ")]");
  71. }
  72. setSelectedPositionInt(mNextSelectedPosition);
  73. // Pull all children into the RecycleBin.
  74. // These views will be reused if possible
  75. final int firstPosition = mFirstPosition;
  76. final RecycleBin recycleBin = mRecycler;
  77. // reset the focus restoration
  78. View focusLayoutRestoreDirectChild = null;
  79. // Don't put header or footer views into the Recycler. Those are
  80. // already cached in mHeaderViews;
  81. if (dataChanged) {
  82. for (int i = 0; i < childCount; i++) {
  83. recycleBin.addScrapView(getChildAt(i));
  84. if (ViewDebug.TRACE_RECYCLER) {
  85. ViewDebug.trace(getChildAt(i),
  86. ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
  87. }
  88. }
  89. } else {
  90. recycleBin.fillActiveViews(childCount, firstPosition);
  91. }
  92. // take focus back to us temporarily to avoid the eventual
  93. // call to clear focus when removing the focused child below
  94. // from messing things up when ViewRoot assigns focus back
  95. // to someone else
  96. final View focusedChild = getFocusedChild();
  97. if (focusedChild != null) {
  98. // TODO: in some cases focusedChild.getParent() == null
  99. // we can remember the focused view to restore after relayout if the
  100. // data hasn't changed, or if the focused position is a header or footer
  101. if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
  102. focusLayoutRestoreDirectChild = focusedChild;
  103. // remember the specific view that had focus
  104. focusLayoutRestoreView = findFocus();
  105. if (focusLayoutRestoreView != null) {
  106. // tell it we are going to mess with it
  107. focusLayoutRestoreView.onStartTemporaryDetach();
  108. }
  109. }
  110. requestFocus();
  111. }
  112. // Clear out old views
  113. detachAllViewsFromParent();
  114. switch (mLayoutMode) {
  115. case LAYOUT_SET_SELECTION:
  116. if (newSel != null) {
  117. sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
  118. } else {
  119. sel = fillFromMiddle(childrenTop, childrenBottom);
  120. }
  121. break;
  122. case LAYOUT_SYNC:
  123. sel = fillSpecific(mSyncPosition, mSpecificTop);
  124. break;
  125. case LAYOUT_FORCE_BOTTOM:
  126. sel = fillUp(mItemCount - 1, childrenBottom);
  127. adjustViewsUpOrDown();
  128. break;
  129. case LAYOUT_FORCE_TOP:
  130. mFirstPosition = 0;
  131. sel = fillFromTop(childrenTop);
  132. adjustViewsUpOrDown();
  133. break;
  134. case LAYOUT_SPECIFIC:
  135. sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
  136. break;
  137. case LAYOUT_MOVE_SELECTION:
  138. sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
  139. break;
  140. default:
  141. if (childCount == 0) {
  142. if (!mStackFromBottom) {
  143. final int position = lookForSelectablePosition(0, true);
  144. setSelectedPositionInt(position);
  145. sel = fillFromTop(childrenTop);
  146. } else {
  147. final int position = lookForSelectablePosition(mItemCount - 1, false);
  148. setSelectedPositionInt(position);
  149. sel = fillUp(mItemCount - 1, childrenBottom);
  150. }
  151. } else {
  152. if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
  153. sel = fillSpecific(mSelectedPosition,
  154. oldSel == null ? childrenTop : oldSel.getTop());
  155. } else if (mFirstPosition < mItemCount) {
  156. sel = fillSpecific(mFirstPosition,
  157. oldFirst == null ? childrenTop : oldFirst.getTop());
  158. } else {
  159. sel = fillSpecific(0, childrenTop);
  160. }
  161. }
  162. break;
  163. }
  164. // Flush any cached views that did not get reused above
  165. recycleBin.scrapActiveViews();
  166. if (sel != null) {
  167. // the current selected item should get focus if items
  168. // are focusable
  169. if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
  170. final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
  171. focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
  172. if (!focusWasTaken) {
  173. // selected item didn't take focus, fine, but still want
  174. // to make sure something else outside of the selected view
  175. // has focus
  176. final View focused = getFocusedChild();
  177. if (focused != null) {
  178. focused.clearFocus();
  179. }
  180. positionSelector(sel);
  181. } else {
  182. sel.setSelected(false);
  183. mSelectorRect.setEmpty();
  184. }
  185. } else {
  186. positionSelector(sel);
  187. }
  188. mSelectedTop = sel.getTop();
  189. } else {
  190. if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
  191. View child = getChildAt(mMotionPosition - mFirstPosition);
  192. if (child != null) positionSelector(child);
  193. } else {
  194. mSelectedTop = 0;
  195. mSelectorRect.setEmpty();
  196. }
  197. // even if there is not selected position, we may need to restore
  198. // focus (i.e. something focusable in touch mode)
  199. if (hasFocus() && focusLayoutRestoreView != null) {
  200. focusLayoutRestoreView.requestFocus();
  201. }
  202. }
  203. // tell focus view we are done mucking with it, if it is still in
  204. // our view hierarchy.
  205. if (focusLayoutRestoreView != null
  206. && focusLayoutRestoreView.getWindowToken() != null) {
  207. focusLayoutRestoreView.onFinishTemporaryDetach();
  208. }
  209. mLayoutMode = LAYOUT_NORMAL;
  210. mDataChanged = false;
  211. mNeedSync = false;
  212. setNextSelectedPositionInt(mSelectedPosition);
  213. updateScrollIndicators();
  214. if (mItemCount > 0) {
  215. checkSelectionChanged();
  216. }
  217. invokeOnItemScrollListener();
  218. } finally {
  219. if (!blockLayoutRequests) {
  220. mBlockLayoutRequests = false;
  221. }
  222. }
  223. }

这段代码比较长,我们挑重点的看。首先可以确定的是,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()方法,我们跟进去瞧一瞧:

[java] view
plain
copy

  1. /**
  2. * Fills the list from top to bottom, starting with mFirstPosition
  3. *
  4. * @param nextTop The location where the top of the first item should be
  5. *        drawn
  6. *
  7. * @return The view that is currently selected
  8. */
  9. private View fillFromTop(int nextTop) {
  10. mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
  11. mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
  12. if (mFirstPosition < 0) {
  13. mFirstPosition = 0;
  14. }
  15. return fillDown(mFirstPosition, nextTop);
  16. }

从这个方法的注释中可以看出,它所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法本身并没有什么逻辑,就是判断了一下mFirstPosition值的合法性,然后调用fillDown()方法,那么我们就有理由可以猜测,填充ListView的操作是在fillDown()方法中完成的。进入fillDown()方法,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Fills the list from pos down to the end of the list view.
  3. *
  4. * @param pos The first position to put in the list
  5. *
  6. * @param nextTop The location where the top of the item associated with pos
  7. *        should be drawn
  8. *
  9. * @return The view that is currently selected, if it happens to be in the
  10. *         range that we draw.
  11. */
  12. private View fillDown(int pos, int nextTop) {
  13. View selectedView = null;
  14. int end = (getBottom() - getTop()) - mListPadding.bottom;
  15. while (nextTop < end && pos < mItemCount) {
  16. // is this the selected item?
  17. boolean selected = pos == mSelectedPosition;
  18. View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
  19. nextTop = child.getBottom() + mDividerHeight;
  20. if (selected) {
  21. selectedView = child;
  22. }
  23. pos++;
  24. }
  25. return selectedView;
  26. }

可以看到,这里使用了一个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()方法,进入到这个方法当中,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Obtain the view and add it to our list of children. The view can be made
  3. * fresh, converted from an unused view, or used as is if it was in the
  4. * recycle bin.
  5. *
  6. * @param position Logical position in the list
  7. * @param y Top or bottom edge of the view to add
  8. * @param flow If flow is true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @return View that was added
  13. */
  14. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  15. boolean selected) {
  16. View child;
  17. if (!mDataChanged) {
  18. // Try to use an exsiting view for this position
  19. child = mRecycler.getActiveView(position);
  20. if (child != null) {
  21. // Found it -- we're using an existing child
  22. // This just needs to be positioned
  23. setupChild(child, position, y, flow, childrenLeft, selected, true);
  24. return child;
  25. }
  26. }
  27. // Make a new view for this position, or convert an unused view if possible
  28. child = obtainView(position, mIsScrap);
  29. // This needs to be positioned and measured
  30. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  31. return child;
  32. }

这里在第19行尝试从RecycleBin当中快速获取一个active view,不过很遗憾的是目前RecycleBin当中还没有缓存任何的View,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行,到第28行会调用obtainView()方法来再次尝试获取一个View,这次的obtainView()方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中。那么obtainView()内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:

[java] view
plain
copy

  1. /**
  2. * Get a view and have it show the data associated with the specified
  3. * position. This is called when we have already discovered that the view is
  4. * not available for reuse in the recycle bin. The only choices left are
  5. * converting an old view or making a new one.
  6. *
  7. * @param position
  8. *            The position to display
  9. * @param isScrap
  10. *            Array of at least 1 boolean, the first entry will become true
  11. *            if the returned view was taken from the scrap heap, false if
  12. *            otherwise.
  13. *
  14. * @return A view displaying the data associated with the specified position
  15. */
  16. View obtainView(int position, boolean[] isScrap) {
  17. isScrap[0] = false;
  18. View scrapView;
  19. scrapView = mRecycler.getScrapView(position);
  20. View child;
  21. if (scrapView != null) {
  22. child = mAdapter.getView(position, scrapView, this);
  23. if (child != scrapView) {
  24. mRecycler.addScrapView(scrapView);
  25. if (mCacheColorHint != 0) {
  26. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  27. }
  28. } else {
  29. isScrap[0] = true;
  30. dispatchFinishTemporaryDetach(child);
  31. }
  32. } else {
  33. child = mAdapter.getView(position, null, this);
  34. if (mCacheColorHint != 0) {
  35. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  36. }
  37. }
  38. return child;
  39. }

obtainView()方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,同样的道理,这里肯定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?当然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。

那么我们平时写ListView的Adapter时,getView()方法通常会怎么写呢?这里我举个简单的例子:

[java] view
plain
copy

  1. @Override
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. Fruit fruit = getItem(position);
  4. View view;
  5. if (convertView == null) {
  6. view = LayoutInflater.from(getContext()).inflate(resourceId, null);
  7. } else {
  8. view = convertView;
  9. }
  10. ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
  11. TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
  12. fruitImage.setImageResource(fruit.getImageId());
  13. fruitName.setText(fruit.getName());
  14. return view;
  15. }

getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。

那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:

[java] view
plain
copy

  1. /**
  2. * Add a view as a child and make sure it is measured (if necessary) and
  3. * positioned properly.
  4. *
  5. * @param child The view to add
  6. * @param position The position of this child
  7. * @param y The y position relative to which this view will be positioned
  8. * @param flowDown If true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @param recycled Has this view been pulled from the recycle bin? If so it
  13. *        does not need to be remeasured.
  14. */
  15. private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
  16. boolean selected, boolean recycled) {
  17. final boolean isSelected = selected && shouldShowSelector();
  18. final boolean updateChildSelected = isSelected != child.isSelected();
  19. final int mode = mTouchMode;
  20. final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
  21. mMotionPosition == position;
  22. final boolean updateChildPressed = isPressed != child.isPressed();
  23. final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
  24. // Respect layout params that are already in the view. Otherwise make some up...
  25. // noinspection unchecked
  26. AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
  27. if (p == null) {
  28. p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
  29. ViewGroup.LayoutParams.WRAP_CONTENT, 0);
  30. }
  31. p.viewType = mAdapter.getItemViewType(position);
  32. if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
  33. p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
  34. attachViewToParent(child, flowDown ? -1 : 0, p);
  35. } else {
  36. p.forceAdd = false;
  37. if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  38. p.recycledHeaderFooter = true;
  39. }
  40. addViewInLayout(child, flowDown ? -1 : 0, p, true);
  41. }
  42. if (updateChildSelected) {
  43. child.setSelected(isSelected);
  44. }
  45. if (updateChildPressed) {
  46. child.setPressed(isPressed);
  47. }
  48. if (needToMeasure) {
  49. int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
  50. mListPadding.left + mListPadding.right, p.width);
  51. int lpHeight = p.height;
  52. int childHeightSpec;
  53. if (lpHeight > 0) {
  54. childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
  55. } else {
  56. childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  57. }
  58. child.measure(childWidthSpec, childHeightSpec);
  59. } else {
  60. cleanupLayoutState(child);
  61. }
  62. final int w = child.getMeasuredWidth();
  63. final int h = child.getMeasuredHeight();
  64. final int childTop = flowDown ? y : y - h;
  65. if (needToMeasure) {
  66. final int childRight = childrenLeft + w;
  67. final int childBottom = childTop + h;
  68. child.layout(childrenLeft, childTop, childRight, childBottom);
  69. } else {
  70. child.offsetLeftAndRight(childrenLeft - child.getLeft());
  71. child.offsetTopAndBottom(childTop - child.getTop());
  72. }
  73. if (mCachingStarted && !child.isDrawingCacheEnabled()) {
  74. child.setDrawingCacheEnabled(true);
  75. }
  76. }

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()方法开始看起:

[java] view
plain
copy

  1. @Override
  2. protected void layoutChildren() {
  3. final boolean blockLayoutRequests = mBlockLayoutRequests;
  4. if (!blockLayoutRequests) {
  5. mBlockLayoutRequests = true;
  6. } else {
  7. return;
  8. }
  9. try {
  10. super.layoutChildren();
  11. invalidate();
  12. if (mAdapter == null) {
  13. resetList();
  14. invokeOnItemScrollListener();
  15. return;
  16. }
  17. int childrenTop = mListPadding.top;
  18. int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
  19. int childCount = getChildCount();
  20. int index = 0;
  21. int delta = 0;
  22. View sel;
  23. View oldSel = null;
  24. View oldFirst = null;
  25. View newSel = null;
  26. View focusLayoutRestoreView = null;
  27. // Remember stuff we will need down below
  28. switch (mLayoutMode) {
  29. case LAYOUT_SET_SELECTION:
  30. index = mNextSelectedPosition - mFirstPosition;
  31. if (index >= 0 && index < childCount) {
  32. newSel = getChildAt(index);
  33. }
  34. break;
  35. case LAYOUT_FORCE_TOP:
  36. case LAYOUT_FORCE_BOTTOM:
  37. case LAYOUT_SPECIFIC:
  38. case LAYOUT_SYNC:
  39. break;
  40. case LAYOUT_MOVE_SELECTION:
  41. default:
  42. // Remember the previously selected view
  43. index = mSelectedPosition - mFirstPosition;
  44. if (index >= 0 && index < childCount) {
  45. oldSel = getChildAt(index);
  46. }
  47. // Remember the previous first child
  48. oldFirst = getChildAt(0);
  49. if (mNextSelectedPosition >= 0) {
  50. delta = mNextSelectedPosition - mSelectedPosition;
  51. }
  52. // Caution: newSel might be null
  53. newSel = getChildAt(index + delta);
  54. }
  55. boolean dataChanged = mDataChanged;
  56. if (dataChanged) {
  57. handleDataChanged();
  58. }
  59. // Handle the empty set by removing all views that are visible
  60. // and calling it a day
  61. if (mItemCount == 0) {
  62. resetList();
  63. invokeOnItemScrollListener();
  64. return;
  65. } else if (mItemCount != mAdapter.getCount()) {
  66. throw new IllegalStateException("The content of the adapter has changed but "
  67. + "ListView did not receive a notification. Make sure the content of "
  68. + "your adapter is not modified from a background thread, but only "
  69. + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
  70. + ") with Adapter(" + mAdapter.getClass() + ")]");
  71. }
  72. setSelectedPositionInt(mNextSelectedPosition);
  73. // Pull all children into the RecycleBin.
  74. // These views will be reused if possible
  75. final int firstPosition = mFirstPosition;
  76. final RecycleBin recycleBin = mRecycler;
  77. // reset the focus restoration
  78. View focusLayoutRestoreDirectChild = null;
  79. // Don't put header or footer views into the Recycler. Those are
  80. // already cached in mHeaderViews;
  81. if (dataChanged) {
  82. for (int i = 0; i < childCount; i++) {
  83. recycleBin.addScrapView(getChildAt(i));
  84. if (ViewDebug.TRACE_RECYCLER) {
  85. ViewDebug.trace(getChildAt(i),
  86. ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
  87. }
  88. }
  89. } else {
  90. recycleBin.fillActiveViews(childCount, firstPosition);
  91. }
  92. // take focus back to us temporarily to avoid the eventual
  93. // call to clear focus when removing the focused child below
  94. // from messing things up when ViewRoot assigns focus back
  95. // to someone else
  96. final View focusedChild = getFocusedChild();
  97. if (focusedChild != null) {
  98. // TODO: in some cases focusedChild.getParent() == null
  99. // we can remember the focused view to restore after relayout if the
  100. // data hasn't changed, or if the focused position is a header or footer
  101. if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
  102. focusLayoutRestoreDirectChild = focusedChild;
  103. // remember the specific view that had focus
  104. focusLayoutRestoreView = findFocus();
  105. if (focusLayoutRestoreView != null) {
  106. // tell it we are going to mess with it
  107. focusLayoutRestoreView.onStartTemporaryDetach();
  108. }
  109. }
  110. requestFocus();
  111. }
  112. // Clear out old views
  113. detachAllViewsFromParent();
  114. switch (mLayoutMode) {
  115. case LAYOUT_SET_SELECTION:
  116. if (newSel != null) {
  117. sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
  118. } else {
  119. sel = fillFromMiddle(childrenTop, childrenBottom);
  120. }
  121. break;
  122. case LAYOUT_SYNC:
  123. sel = fillSpecific(mSyncPosition, mSpecificTop);
  124. break;
  125. case LAYOUT_FORCE_BOTTOM:
  126. sel = fillUp(mItemCount - 1, childrenBottom);
  127. adjustViewsUpOrDown();
  128. break;
  129. case LAYOUT_FORCE_TOP:
  130. mFirstPosition = 0;
  131. sel = fillFromTop(childrenTop);
  132. adjustViewsUpOrDown();
  133. break;
  134. case LAYOUT_SPECIFIC:
  135. sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
  136. break;
  137. case LAYOUT_MOVE_SELECTION:
  138. sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
  139. break;
  140. default:
  141. if (childCount == 0) {
  142. if (!mStackFromBottom) {
  143. final int position = lookForSelectablePosition(0, true);
  144. setSelectedPositionInt(position);
  145. sel = fillFromTop(childrenTop);
  146. } else {
  147. final int position = lookForSelectablePosition(mItemCount - 1, false);
  148. setSelectedPositionInt(position);
  149. sel = fillUp(mItemCount - 1, childrenBottom);
  150. }
  151. } else {
  152. if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
  153. sel = fillSpecific(mSelectedPosition,
  154. oldSel == null ? childrenTop : oldSel.getTop());
  155. } else if (mFirstPosition < mItemCount) {
  156. sel = fillSpecific(mFirstPosition,
  157. oldFirst == null ? childrenTop : oldFirst.getTop());
  158. } else {
  159. sel = fillSpecific(0, childrenTop);
  160. }
  161. }
  162. break;
  163. }
  164. // Flush any cached views that did not get reused above
  165. recycleBin.scrapActiveViews();
  166. if (sel != null) {
  167. // the current selected item should get focus if items
  168. // are focusable
  169. if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
  170. final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
  171. focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
  172. if (!focusWasTaken) {
  173. // selected item didn't take focus, fine, but still want
  174. // to make sure something else outside of the selected view
  175. // has focus
  176. final View focused = getFocusedChild();
  177. if (focused != null) {
  178. focused.clearFocus();
  179. }
  180. positionSelector(sel);
  181. } else {
  182. sel.setSelected(false);
  183. mSelectorRect.setEmpty();
  184. }
  185. } else {
  186. positionSelector(sel);
  187. }
  188. mSelectedTop = sel.getTop();
  189. } else {
  190. if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
  191. View child = getChildAt(mMotionPosition - mFirstPosition);
  192. if (child != null) positionSelector(child);
  193. } else {
  194. mSelectedTop = 0;
  195. mSelectorRect.setEmpty();
  196. }
  197. // even if there is not selected position, we may need to restore
  198. // focus (i.e. something focusable in touch mode)
  199. if (hasFocus() && focusLayoutRestoreView != null) {
  200. focusLayoutRestoreView.requestFocus();
  201. }
  202. }
  203. // tell focus view we are done mucking with it, if it is still in
  204. // our view hierarchy.
  205. if (focusLayoutRestoreView != null
  206. && focusLayoutRestoreView.getWindowToken() != null) {
  207. focusLayoutRestoreView.onFinishTemporaryDetach();
  208. }
  209. mLayoutMode = LAYOUT_NORMAL;
  210. mDataChanged = false;
  211. mNeedSync = false;
  212. setNextSelectedPositionInt(mSelectedPosition);
  213. updateScrollIndicators();
  214. if (mItemCount > 0) {
  215. checkSelectionChanged();
  216. }
  217. invokeOnItemScrollListener();
  218. } finally {
  219. if (!blockLayoutRequests) {
  220. mBlockLayoutRequests = false;
  221. }
  222. }
  223. }

同样还是在第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()方法当中,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Put a specific item at a specific location on the screen and then build
  3. * up and down from there.
  4. *
  5. * @param position The reference view to use as the starting point
  6. * @param top Pixel offset from the top of this view to the top of the
  7. *        reference view.
  8. *
  9. * @return The selected view, or null if the selected view is outside the
  10. *         visible area.
  11. */
  12. private View fillSpecific(int position, int top) {
  13. boolean tempIsSelected = position == mSelectedPosition;
  14. View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
  15. // Possibly changed again in fillUp if we add rows above this one.
  16. mFirstPosition = position;
  17. View above;
  18. View below;
  19. final int dividerHeight = mDividerHeight;
  20. if (!mStackFromBottom) {
  21. above = fillUp(position - 1, temp.getTop() - dividerHeight);
  22. // This will correct for the top of the first view not touching the top of the list
  23. adjustViewsUpOrDown();
  24. below = fillDown(position + 1, temp.getBottom() + dividerHeight);
  25. int childCount = getChildCount();
  26. if (childCount > 0) {
  27. correctTooHigh(childCount);
  28. }
  29. } else {
  30. below = fillDown(position + 1, temp.getBottom() + dividerHeight);
  31. // This will correct for the bottom of the last view not touching the bottom of the list
  32. adjustViewsUpOrDown();
  33. above = fillUp(position - 1, temp.getTop() - dividerHeight);
  34. int childCount = getChildCount();
  35. if (childCount > 0) {
  36. correctTooLow(childCount);
  37. }
  38. }
  39. if (tempIsSelected) {
  40. return temp;
  41. } else if (above != null) {
  42. return above;
  43. } else {
  44. return below;
  45. }
  46. }

fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Obtain the view and add it to our list of children. The view can be made
  3. * fresh, converted from an unused view, or used as is if it was in the
  4. * recycle bin.
  5. *
  6. * @param position Logical position in the list
  7. * @param y Top or bottom edge of the view to add
  8. * @param flow If flow is true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @return View that was added
  13. */
  14. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  15. boolean selected) {
  16. View child;
  17. if (!mDataChanged) {
  18. // Try to use an exsiting view for this position
  19. child = mRecycler.getActiveView(position);
  20. if (child != null) {
  21. // Found it -- we're using an existing child
  22. // This just needs to be positioned
  23. setupChild(child, position, y, flow, childrenLeft, selected, true);
  24. return child;
  25. }
  26. }
  27. // Make a new view for this position, or convert an unused view if possible
  28. child = obtainView(position, mIsScrap);
  29. // This needs to be positioned and measured
  30. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  31. return child;
  32. }

仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。

注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:

[java] view
plain
copy

  1. /**
  2. * Add a view as a child and make sure it is measured (if necessary) and
  3. * positioned properly.
  4. *
  5. * @param child The view to add
  6. * @param position The position of this child
  7. * @param y The y position relative to which this view will be positioned
  8. * @param flowDown If true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @param recycled Has this view been pulled from the recycle bin? If so it
  13. *        does not need to be remeasured.
  14. */
  15. private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
  16. boolean selected, boolean recycled) {
  17. final boolean isSelected = selected && shouldShowSelector();
  18. final boolean updateChildSelected = isSelected != child.isSelected();
  19. final int mode = mTouchMode;
  20. final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
  21. mMotionPosition == position;
  22. final boolean updateChildPressed = isPressed != child.isPressed();
  23. final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
  24. // Respect layout params that are already in the view. Otherwise make some up...
  25. // noinspection unchecked
  26. AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
  27. if (p == null) {
  28. p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
  29. ViewGroup.LayoutParams.WRAP_CONTENT, 0);
  30. }
  31. p.viewType = mAdapter.getItemViewType(position);
  32. if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
  33. p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
  34. attachViewToParent(child, flowDown ? -1 : 0, p);
  35. } else {
  36. p.forceAdd = false;
  37. if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
  38. p.recycledHeaderFooter = true;
  39. }
  40. addViewInLayout(child, flowDown ? -1 : 0, p, true);
  41. }
  42. if (updateChildSelected) {
  43. child.setSelected(isSelected);
  44. }
  45. if (updateChildPressed) {
  46. child.setPressed(isPressed);
  47. }
  48. if (needToMeasure) {
  49. int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
  50. mListPadding.left + mListPadding.right, p.width);
  51. int lpHeight = p.height;
  52. int childHeightSpec;
  53. if (lpHeight > 0) {
  54. childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
  55. } else {
  56. childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  57. }
  58. child.measure(childWidthSpec, childHeightSpec);
  59. } else {
  60. cleanupLayoutState(child);
  61. }
  62. final int w = child.getMeasuredWidth();
  63. final int h = child.getMeasuredHeight();
  64. final int childTop = flowDown ? y : y - h;
  65. if (needToMeasure) {
  66. final int childRight = childrenLeft + w;
  67. final int childBottom = childTop + h;
  68. child.layout(childrenLeft, childTop, childRight, childBottom);
  69. } else {
  70. child.offsetLeftAndRight(childrenLeft - child.getLeft());
  71. child.offsetTopAndBottom(childTop - child.getTop());
  72. }
  73. if (mCachingStarted && !child.isDrawingCacheEnabled()) {
  74. child.setDrawingCacheEnabled(true);
  75. }
  76. }

可以看到,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中的这个方法:

[java] view
plain
copy

  1. @Override
  2. public boolean onTouchEvent(MotionEvent ev) {
  3. if (!isEnabled()) {
  4. // A disabled view that is clickable still consumes the touch
  5. // events, it just doesn't respond to them.
  6. return isClickable() || isLongClickable();
  7. }
  8. final int action = ev.getAction();
  9. View v;
  10. int deltaY;
  11. if (mVelocityTracker == null) {
  12. mVelocityTracker = VelocityTracker.obtain();
  13. }
  14. mVelocityTracker.addMovement(ev);
  15. switch (action & MotionEvent.ACTION_MASK) {
  16. case MotionEvent.ACTION_DOWN: {
  17. mActivePointerId = ev.getPointerId(0);
  18. final int x = (int) ev.getX();
  19. final int y = (int) ev.getY();
  20. int motionPosition = pointToPosition(x, y);
  21. if (!mDataChanged) {
  22. if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
  23. && (getAdapter().isEnabled(motionPosition))) {
  24. // User clicked on an actual view (and was not stopping a
  25. // fling). It might be a
  26. // click or a scroll. Assume it is a click until proven
  27. // otherwise
  28. mTouchMode = TOUCH_MODE_DOWN;
  29. // FIXME Debounce
  30. if (mPendingCheckForTap == null) {
  31. mPendingCheckForTap = new CheckForTap();
  32. }
  33. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  34. } else {
  35. if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
  36. // If we couldn't find a view to click on, but the down
  37. // event was touching
  38. // the edge, we will bail out and try again. This allows
  39. // the edge correcting
  40. // code in ViewRoot to try to find a nearby view to
  41. // select
  42. return false;
  43. }
  44. if (mTouchMode == TOUCH_MODE_FLING) {
  45. // Stopped a fling. It is a scroll.
  46. createScrollingCache();
  47. mTouchMode = TOUCH_MODE_SCROLL;
  48. mMotionCorrection = 0;
  49. motionPosition = findMotionRow(y);
  50. reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
  51. }
  52. }
  53. }
  54. if (motionPosition >= 0) {
  55. // Remember where the motion event started
  56. v = getChildAt(motionPosition - mFirstPosition);
  57. mMotionViewOriginalTop = v.getTop();
  58. }
  59. mMotionX = x;
  60. mMotionY = y;
  61. mMotionPosition = motionPosition;
  62. mLastY = Integer.MIN_VALUE;
  63. break;
  64. }
  65. case MotionEvent.ACTION_MOVE: {
  66. final int pointerIndex = ev.findPointerIndex(mActivePointerId);
  67. final int y = (int) ev.getY(pointerIndex);
  68. deltaY = y - mMotionY;
  69. switch (mTouchMode) {
  70. case TOUCH_MODE_DOWN:
  71. case TOUCH_MODE_TAP:
  72. case TOUCH_MODE_DONE_WAITING:
  73. // Check if we have moved far enough that it looks more like a
  74. // scroll than a tap
  75. startScrollIfNeeded(deltaY);
  76. break;
  77. case TOUCH_MODE_SCROLL:
  78. if (PROFILE_SCROLLING) {
  79. if (!mScrollProfilingStarted) {
  80. Debug.startMethodTracing("AbsListViewScroll");
  81. mScrollProfilingStarted = true;
  82. }
  83. }
  84. if (y != mLastY) {
  85. deltaY -= mMotionCorrection;
  86. int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
  87. // No need to do all this work if we're not going to move
  88. // anyway
  89. boolean atEdge = false;
  90. if (incrementalDeltaY != 0) {
  91. atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
  92. }
  93. // Check to see if we have bumped into the scroll limit
  94. if (atEdge && getChildCount() > 0) {
  95. // Treat this like we're starting a new scroll from the
  96. // current
  97. // position. This will let the user start scrolling back
  98. // into
  99. // content immediately rather than needing to scroll
  100. // back to the
  101. // point where they hit the limit first.
  102. int motionPosition = findMotionRow(y);
  103. if (motionPosition >= 0) {
  104. final View motionView = getChildAt(motionPosition - mFirstPosition);
  105. mMotionViewOriginalTop = motionView.getTop();
  106. }
  107. mMotionY = y;
  108. mMotionPosition = motionPosition;
  109. invalidate();
  110. }
  111. mLastY = y;
  112. }
  113. break;
  114. }
  115. break;
  116. }
  117. case MotionEvent.ACTION_UP: {
  118. switch (mTouchMode) {
  119. case TOUCH_MODE_DOWN:
  120. case TOUCH_MODE_TAP:
  121. case TOUCH_MODE_DONE_WAITING:
  122. final int motionPosition = mMotionPosition;
  123. final View child = getChildAt(motionPosition - mFirstPosition);
  124. if (child != null && !child.hasFocusable()) {
  125. if (mTouchMode != TOUCH_MODE_DOWN) {
  126. child.setPressed(false);
  127. }
  128. if (mPerformClick == null) {
  129. mPerformClick = new PerformClick();
  130. }
  131. final AbsListView.PerformClick performClick = mPerformClick;
  132. performClick.mChild = child;
  133. performClick.mClickMotionPosition = motionPosition;
  134. performClick.rememberWindowAttachCount();
  135. mResurrectToPosition = motionPosition;
  136. if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
  137. final Handler handler = getHandler();
  138. if (handler != null) {
  139. handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
  140. : mPendingCheckForLongPress);
  141. }
  142. mLayoutMode = LAYOUT_NORMAL;
  143. if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  144. mTouchMode = TOUCH_MODE_TAP;
  145. setSelectedPositionInt(mMotionPosition);
  146. layoutChildren();
  147. child.setPressed(true);
  148. positionSelector(child);
  149. setPressed(true);
  150. if (mSelector != null) {
  151. Drawable d = mSelector.getCurrent();
  152. if (d != null && d instanceof TransitionDrawable) {
  153. ((TransitionDrawable) d).resetTransition();
  154. }
  155. }
  156. postDelayed(new Runnable() {
  157. public void run() {
  158. child.setPressed(false);
  159. setPressed(false);
  160. if (!mDataChanged) {
  161. post(performClick);
  162. }
  163. mTouchMode = TOUCH_MODE_REST;
  164. }
  165. }, ViewConfiguration.getPressedStateDuration());
  166. } else {
  167. mTouchMode = TOUCH_MODE_REST;
  168. }
  169. return true;
  170. } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  171. post(performClick);
  172. }
  173. }
  174. mTouchMode = TOUCH_MODE_REST;
  175. break;
  176. case TOUCH_MODE_SCROLL:
  177. final int childCount = getChildCount();
  178. if (childCount > 0) {
  179. if (mFirstPosition == 0
  180. && getChildAt(0).getTop() >= mListPadding.top
  181. && mFirstPosition + childCount < mItemCount
  182. && getChildAt(childCount - 1).getBottom() <= getHeight()
  183. - mListPadding.bottom) {
  184. mTouchMode = TOUCH_MODE_REST;
  185. reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  186. } else {
  187. final VelocityTracker velocityTracker = mVelocityTracker;
  188. velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  189. final int initialVelocity = (int) velocityTracker
  190. .getYVelocity(mActivePointerId);
  191. if (Math.abs(initialVelocity) > mMinimumVelocity) {
  192. if (mFlingRunnable == null) {
  193. mFlingRunnable = new FlingRunnable();
  194. }
  195. reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
  196. mFlingRunnable.start(-initialVelocity);
  197. } else {
  198. mTouchMode = TOUCH_MODE_REST;
  199. reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  200. }
  201. }
  202. } else {
  203. mTouchMode = TOUCH_MODE_REST;
  204. reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  205. }
  206. break;
  207. }
  208. setPressed(false);
  209. // Need to redraw since we probably aren't drawing the selector
  210. // anymore
  211. invalidate();
  212. final Handler handler = getHandler();
  213. if (handler != null) {
  214. handler.removeCallbacks(mPendingCheckForLongPress);
  215. }
  216. if (mVelocityTracker != null) {
  217. mVelocityTracker.recycle();
  218. mVelocityTracker = null;
  219. }
  220. mActivePointerId = INVALID_POINTER;
  221. if (PROFILE_SCROLLING) {
  222. if (mScrollProfilingStarted) {
  223. Debug.stopMethodTracing();
  224. mScrollProfilingStarted = false;
  225. }
  226. }
  227. break;
  228. }
  229. case MotionEvent.ACTION_CANCEL: {
  230. mTouchMode = TOUCH_MODE_REST;
  231. setPressed(false);
  232. View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
  233. if (motionView != null) {
  234. motionView.setPressed(false);
  235. }
  236. clearScrollingCache();
  237. final Handler handler = getHandler();
  238. if (handler != null) {
  239. handler.removeCallbacks(mPendingCheckForLongPress);
  240. }
  241. if (mVelocityTracker != null) {
  242. mVelocityTracker.recycle();
  243. mVelocityTracker = null;
  244. }
  245. mActivePointerId = INVALID_POINTER;
  246. break;
  247. }
  248. case MotionEvent.ACTION_POINTER_UP: {
  249. onSecondaryPointerUp(ev);
  250. final int x = mMotionX;
  251. final int y = mMotionY;
  252. final int motionPosition = pointToPosition(x, y);
  253. if (motionPosition >= 0) {
  254. // Remember where the motion event started
  255. v = getChildAt(motionPosition - mFirstPosition);
  256. mMotionViewOriginalTop = v.getTop();
  257. mMotionPosition = motionPosition;
  258. }
  259. mLastY = y;
  260. break;
  261. }
  262. }
  263. return true;
  264. }

这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,那么我们就只看这部分代码就可以了。

可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解了,喜欢寻根究底的朋友们可以自己去源码里找一找原因。

这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并没有什么太多需要注意的东西,唯一一点非常重要的就是第92行调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。那么我们进入到这个方法中瞧一瞧,代码如下所示:

[java] view
plain
copy

  1. boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
  2. final int childCount = getChildCount();
  3. if (childCount == 0) {
  4. return true;
  5. }
  6. final int firstTop = getChildAt(0).getTop();
  7. final int lastBottom = getChildAt(childCount - 1).getBottom();
  8. final Rect listPadding = mListPadding;
  9. final int spaceAbove = listPadding.top - firstTop;
  10. final int end = getHeight() - listPadding.bottom;
  11. final int spaceBelow = lastBottom - end;
  12. final int height = getHeight() - getPaddingBottom() - getPaddingTop();
  13. if (deltaY < 0) {
  14. deltaY = Math.max(-(height - 1), deltaY);
  15. } else {
  16. deltaY = Math.min(height - 1, deltaY);
  17. }
  18. if (incrementalDeltaY < 0) {
  19. incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
  20. } else {
  21. incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
  22. }
  23. final int firstPosition = mFirstPosition;
  24. if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
  25. // Don't need to move views down if the top of the first position
  26. // is already visible
  27. return true;
  28. }
  29. if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
  30. // Don't need to move views up if the bottom of the last position
  31. // is already visible
  32. return true;
  33. }
  34. final boolean down = incrementalDeltaY < 0;
  35. final boolean inTouchMode = isInTouchMode();
  36. if (inTouchMode) {
  37. hideSelector();
  38. }
  39. final int headerViewsCount = getHeaderViewsCount();
  40. final int footerViewsStart = mItemCount - getFooterViewsCount();
  41. int start = 0;
  42. int count = 0;
  43. if (down) {
  44. final int top = listPadding.top - incrementalDeltaY;
  45. for (int i = 0; i < childCount; i++) {
  46. final View child = getChildAt(i);
  47. if (child.getBottom() >= top) {
  48. break;
  49. } else {
  50. count++;
  51. int position = firstPosition + i;
  52. if (position >= headerViewsCount && position < footerViewsStart) {
  53. mRecycler.addScrapView(child);
  54. }
  55. }
  56. }
  57. } else {
  58. final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
  59. for (int i = childCount - 1; i >= 0; i--) {
  60. final View child = getChildAt(i);
  61. if (child.getTop() <= bottom) {
  62. break;
  63. } else {
  64. start = i;
  65. count++;
  66. int position = firstPosition + i;
  67. if (position >= headerViewsCount && position < footerViewsStart) {
  68. mRecycler.addScrapView(child);
  69. }
  70. }
  71. }
  72. }
  73. mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
  74. mBlockLayoutRequests = true;
  75. if (count > 0) {
  76. detachViewsFromParent(start, count);
  77. }
  78. offsetChildrenTopAndBottom(incrementalDeltaY);
  79. if (down) {
  80. mFirstPosition += count;
  81. }
  82. invalidate();
  83. final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
  84. if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
  85. fillGap(down);
  86. }
  87. if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
  88. final int childIndex = mSelectedPosition - mFirstPosition;
  89. if (childIndex >= 0 && childIndex < getChildCount()) {
  90. positionSelector(getChildAt(childIndex));
  91. }
  92. }
  93. mBlockLayoutRequests = false;
  94. invokeOnItemScrollListener();
  95. awakenScrollBars();
  96. return false;
  97. }

这个方法接收两个参数,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()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,如下所示:

[java] view
plain
copy

  1. /**
  2. * Fills the gap left open by a touch-scroll. During a touch scroll,
  3. * children that remain on screen are shifted and the other ones are
  4. * discarded. The role of this method is to fill the gap thus created by
  5. * performing a partial layout in the empty space.
  6. *
  7. * @param down
  8. *            true if the scroll is going down, false if it is going up
  9. */
  10. abstract void fillGap(boolean down);

OK,AbsListView中的fillGap()是一个抽象方法,那么我们立刻就能够想到,它的具体实现肯定是在ListView中完成的了。回到ListView当中,fillGap()方法的代码如下所示:

[java] view
plain
copy

  1. void fillGap(boolean down) {
  2. final int count = getChildCount();
  3. if (down) {
  4. final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
  5. getListPaddingTop();
  6. fillDown(mFirstPosition + count, startOffset);
  7. correctTooHigh(getChildCount());
  8. } else {
  9. final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
  10. getHeight() - getListPaddingBottom();
  11. fillUp(mFirstPosition - 1, startOffset);
  12. correctTooLow(getChildCount());
  13. }
  14. }

down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:

[java] view
plain
copy

  1. /**
  2. * Obtain the view and add it to our list of children. The view can be made
  3. * fresh, converted from an unused view, or used as is if it was in the
  4. * recycle bin.
  5. *
  6. * @param position Logical position in the list
  7. * @param y Top or bottom edge of the view to add
  8. * @param flow If flow is true, align top edge to y. If false, align bottom
  9. *        edge to y.
  10. * @param childrenLeft Left edge where children should be positioned
  11. * @param selected Is this position selected?
  12. * @return View that was added
  13. */
  14. private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
  15. boolean selected) {
  16. View child;
  17. if (!mDataChanged) {
  18. // Try to use an exsiting view for this position
  19. child = mRecycler.getActiveView(position);
  20. if (child != null) {
  21. // Found it -- we're using an existing child
  22. // This just needs to be positioned
  23. setupChild(child, position, y, flow, childrenLeft, selected, true);
  24. return child;
  25. }
  26. }
  27. // Make a new view for this position, or convert an unused view if possible
  28. child = obtainView(position, mIsScrap);
  29. // This needs to be positioned and measured
  30. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
  31. return child;
  32. }

不管怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。

既然getActiveView()方法返回的值是null,那么就还是会走到第28行的obtainView()方法当中,代码如下所示:

[java] view
plain
copy

  1. /**
  2. * Get a view and have it show the data associated with the specified
  3. * position. This is called when we have already discovered that the view is
  4. * not available for reuse in the recycle bin. The only choices left are
  5. * converting an old view or making a new one.
  6. *
  7. * @param position
  8. *            The position to display
  9. * @param isScrap
  10. *            Array of at least 1 boolean, the first entry will become true
  11. *            if the returned view was taken from the scrap heap, false if
  12. *            otherwise.
  13. *
  14. * @return A view displaying the data associated with the specified position
  15. */
  16. View obtainView(int position, boolean[] isScrap) {
  17. isScrap[0] = false;
  18. View scrapView;
  19. scrapView = mRecycler.getScrapView(position);
  20. View child;
  21. if (scrapView != null) {
  22. child = mAdapter.getView(position, scrapView, this);
  23. if (child != scrapView) {
  24. mRecycler.addScrapView(scrapView);
  25. if (mCacheColorHint != 0) {
  26. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  27. }
  28. } else {
  29. isScrap[0] = true;
  30. dispatchFinishTemporaryDetach(child);
  31. }
  32. } else {
  33. child = mAdapter.getView(position, null, this);
  34. if (mCacheColorHint != 0) {
  35. child.setDrawingCacheBackgroundColor(mCacheColorHint);
  36. }
  37. }
  38. return child;
  39. }

这里在第19行会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,然后我们在第22行将它作为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getView()方法示例:

[java] view
plain
copy

  1. @Override
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. Fruit fruit = getItem(position);
  4. View view;
  5. if (convertView == null) {
  6. view = LayoutInflater.from(getContext()).inflate(resourceId, null);
  7. } else {
  8. view = convertView;
  9. }
  10. ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
  11. TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
  12. fruitImage.setImageResource(fruit.getImageId());
  13. fruitName.setText(fruit.getName());
  14. return view;
  15. }

第二个参数就是我们最熟悉的convertView呀,难怪平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?

之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。

为了方便大家理解,这里我再附上一张图解说明:

那么到目前为止,我们就把ListView的整个工作流程代码基本分析结束了,文章比较长,希望大家可以理解清楚,下篇文章中会讲解我们平时使用ListView时遇到的问题,敬请期待。

listview的工作原理的更多相关文章

  1. Android ListView工作原理完全解析,带你从源码的角度彻底理解

    版权声明:本文出自郭霖的博客,转载必须注明出处.   目录(?)[+] Adapter的作用 RecycleBin机制 第一次Layout 第二次Layout 滑动加载更多数据   转载请注明出处:h ...

  2. Android ListView工作原理完全解析(转自 郭霖老师博客)

    原文地址:http://blog.csdn.net/guolin_blog/article/details/44996879 在Android所有常用的原生控件当中,用法最复杂的应该就是ListVie ...

  3. Android ListView工作原理全然解析,带你从源代码的角度彻底理解

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/44996879 在Android全部经常使用的原生控件其中.使用方法最复杂的应该就是 ...

  4. 转:ListView中getView的工作原理

    ListView中getView的工作原理: [1]ListView asks adapter “give me a view” (getView) for each item of the list ...

  5. Android Widget工作原理详解(一) 最全介绍

    转载请标明出处:http://blog.csdn.net/sk719887916/article/details/46853033 ; Widget是安卓的一应用程序组件,学名窗口小部件,它是微型应用 ...

  6. Android 中View的工作原理

    Android中的View在Android的知识体系中扮演着重要的角色.简单来说,View就是Android在视觉的体现.我们所展现的页面就是Android提供的GUI库中控件的组合.但是当要求不能满 ...

  7. 菜鸟学Struts2——Struts工作原理

    在完成Struts2的HelloWorld后,对Struts2的工作原理进行学习.Struts2框架可以按照模块来划分为Servlet Filters,Struts核心模块,拦截器和用户实现部分,其中 ...

  8. 【夯实Nginx基础】Nginx工作原理和优化、漏洞

    本文地址 原文地址 本文提纲: 1.  Nginx的模块与工作原理    2.  Nginx的进程模型    3 . NginxFastCGI运行原理        3.1 什么是 FastCGI   ...

  9. HashMap的工作原理

    HashMap的工作原理   HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之间 ...

随机推荐

  1. MySQL where 子句

    MySQL where 子句 我们知道从MySQL表中使用SQL SELECT 语句来读取数据. 如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句中. 语法 以下是SQL ...

  2. PHP 文件

    PHP 文件处理 fopen() 函数用于在 PHP 中打开文件. 打开文件 fopen() 函数用于在 PHP 中打开文件. 此函数的第一个参数含有要打开的文件的名称,第二个参数规定了使用哪种模式来 ...

  3. Docker容器如何互联

    容器的连接(linking)系统是除了端口映射外,另一种跟容器中应用交互的方式. 该系统会在源和接收容器之间创建一个隧道,接收容器可以看到源容器指定的信息. 自定义容器命名 连接系统依据容器的名称来执 ...

  4. JMETER_从入门到放弃系列

    基础篇 Jmeter(一)_环境部署 Jmeter(二)_基础元件 Jmeter(三)_配置元件 Jmeter(四)_16个逻辑控制器 Jmeter(五)_24个函数 Jmeter(六)_前置处理器 ...

  5. ROS(indigo) 语音工具 科大讯飞 百度 pocketsphinx julius rospeex 16.11.22更新 ROS中文语音

    ROS语音工具汇总,目前先给出链接,只用过一些简单的命令. 中文语音: 参考链接:使用科大讯飞库 1 http://www.ncnynl.com/archives/201611/1069.html 2 ...

  6. CSS简单使用

    CSS简单使用 标签 : 前端技术 CSS(Cascading Style Sheet : 层叠样式表单)用来定义网页显示效果. 可以解决HTML代码对样式定义的重复,提高后期样式代码的可维护性,并增 ...

  7. 2apt-get命令,deb包安装,源码安装

    1 安装卸载软件 更新源服务器列表 sudovi /etc/apt/sources.list 更新完服务器列表后需要更新下源 sudoapt-get update 更新源 sudoapt-get in ...

  8. Android 访问assets下的文件

    assets下经常可以放一些比较大的资源,对于这些资源我们如何访问. 步骤 1.获取AssetManager. AssetManager am = getResources().getAssets() ...

  9. 6.0、Android Studio性能优化工具

    显示图像包含四个步骤.简单来说,CPU对比显示列表,GPU渲染图片显示,内存存储图片和数据,电池提供点力能源.每个部分的硬件都有限制,超过这个限制会导致应用运行较慢,显示性能差,或者耗电. 为了查找造 ...

  10. Android Handler机制剖析

    android的handler机制是android的线程通信的核心机制 Android UI是线程不安全的,如果在子线程中尝试进行UI操作,程序就有可能会崩溃. Android中的实现了 接收消息的& ...