在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。
另外ListView还有一个功能,达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。ListView的代码量比较大,复杂度也很高,很难用文字表达清楚。
RecycleBin机制
RecycleBin机制是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。其实RecycleBin的代码并不多,只有300行左右,它是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。RecycleBin中的主要代码,如下所示:
- /**
- * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
- * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
- * those views which were onscreen at the start of a layout. By
- * construction, they are displaying current information. At the end of
- * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
- * are old views that could potentially be used by the adapter to avoid
- * allocating views unnecessarily.
- *
- * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
- * @see android.widget.AbsListView.RecyclerListener
- */
- class RecycleBin {
- private RecyclerListener mRecyclerListener;
- /**
- * The position of the first view stored in mActiveViews.
- */
- private int mFirstActivePosition;
- /**
- * Views that were on screen at the start of layout. This array is
- * populated at the start of layout, and at the end of layout all view
- * in mActiveViews are moved to mScrapViews. Views in mActiveViews
- * represent a contiguous range of Views, with position of the first
- * view store in mFirstActivePosition.
- */
- private View[] mActiveViews = new View[0];
- /**
- * Unsorted views that can be used by the adapter as a convert view.
- */
- private ArrayList<View>[] mScrapViews;
- private int mViewTypeCount;
- private ArrayList<View> mCurrentScrap;
- /**
- * Fill ActiveViews with all of the children of the AbsListView.
- *
- * @param childCount
- * The minimum number of views mActiveViews should hold
- * @param firstActivePosition
- * The position of the first view that will be stored in
- * mActiveViews
- */
- void fillActiveViews(int childCount, int firstActivePosition) {
- if (mActiveViews.length < childCount) {
- mActiveViews = new View[childCount];
- }
- mFirstActivePosition = firstActivePosition;
- final View[] activeViews = mActiveViews;
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
- // Don't put header or footer views into the scrap heap
- if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
- // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
- // active views.
- // However, we will NOT place them into scrap views.
- activeViews[i] = child;
- }
- }
- }
- /**
- * Get the view corresponding to the specified position. The view will
- * be removed from mActiveViews if it is found.
- *
- * @param position
- * The position to look up in mActiveViews
- * @return The view if it is found, null otherwise
- */
- View getActiveView(int position) {
- int index = position - mFirstActivePosition;
- final View[] activeViews = mActiveViews;
- if (index >= 0 && index < activeViews.length) {
- final View match = activeViews[index];
- activeViews[index] = null;
- return match;
- }
- return null;
- }
- /**
- * Put a view into the ScapViews list. These views are unordered.
- *
- * @param scrap
- * The view to add
- */
- void addScrapView(View scrap) {
- AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
- if (lp == null) {
- return;
- }
- // Don't put header or footer views or views that should be ignored
- // into the scrap heap
- int viewType = lp.viewType;
- if (!shouldRecycleViewType(viewType)) {
- if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
- removeDetachedView(scrap, false);
- }
- return;
- }
- if (mViewTypeCount == 1) {
- dispatchFinishTemporaryDetach(scrap);
- mCurrentScrap.add(scrap);
- } else {
- dispatchFinishTemporaryDetach(scrap);
- mScrapViews[viewType].add(scrap);
- }
- if (mRecyclerListener != null) {
- mRecyclerListener.onMovedToScrapHeap(scrap);
- }
- }
- /**
- * @return A view from the ScrapViews collection. These are unordered.
- */
- View getScrapView(int position) {
- ArrayList<View> scrapViews;
- if (mViewTypeCount == 1) {
- scrapViews = mCurrentScrap;
- int size = scrapViews.size();
- if (size > 0) {
- return scrapViews.remove(size - 1);
- } else {
- return null;
- }
- } else {
- int whichScrap = mAdapter.getItemViewType(position);
- if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
- scrapViews = mScrapViews[whichScrap];
- int size = scrapViews.size();
- if (size > 0) {
- return scrapViews.remove(size - 1);
- }
- }
- }
- return null;
- }
- public void setViewTypeCount(int viewTypeCount) {
- if (viewTypeCount < 1) {
- throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
- }
- // noinspection unchecked
- ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
- for (int i = 0; i < viewTypeCount; i++) {
- scrapViews[i] = new ArrayList<View>();
- }
- mViewTypeCount = viewTypeCount;
- mCurrentScrap = scrapViews[0];
- mScrapViews = scrapViews;
- }
- }
-
- view plaincopy
- /**
- * Subclasses should NOT override this method but {@link #layoutChildren()}
- * instead.
- */
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- mInLayout = true;
- if (changed) {
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).forceLayout();
- }
- mRecycler.markChildrenDirty();
- }
- layoutChildren();
- mInLayout = false;
- }
- @Override
- protected void layoutChildren() {
- final boolean blockLayoutRequests = mBlockLayoutRequests;
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = true;
- } else {
- return;
- }
- try {
- super.layoutChildren();
- invalidate();
- if (mAdapter == null) {
- resetList();
- invokeOnItemScrollListener();
- return;
- }
- int childrenTop = mListPadding.top;
- int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
- int childCount = getChildCount();
- int index = 0;
- int delta = 0;
- View sel;
- View oldSel = null;
- View oldFirst = null;
- View newSel = null;
- View focusLayoutRestoreView = null;
- // Remember stuff we will need down below
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- index = mNextSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- newSel = getChildAt(index);
- }
- break;
- case LAYOUT_FORCE_TOP:
- case LAYOUT_FORCE_BOTTOM:
- case LAYOUT_SPECIFIC:
- case LAYOUT_SYNC:
- break;
- case LAYOUT_MOVE_SELECTION:
- default:
- // Remember the previously selected view
- index = mSelectedPosition - mFirstPosition;
- if (index >= 0 && index < childCount) {
- oldSel = getChildAt(index);
- }
- // Remember the previous first child
- oldFirst = getChildAt(0);
- if (mNextSelectedPosition >= 0) {
- delta = mNextSelectedPosition - mSelectedPosition;
- }
- // Caution: newSel might be null
- newSel = getChildAt(index + delta);
- }
- boolean dataChanged = mDataChanged;
- if (dataChanged) {
- handleDataChanged();
- }
- // Handle the empty set by removing all views that are visible
- // and calling it a day
- if (mItemCount == 0) {
- resetList();
- invokeOnItemScrollListener();
- return;
- } else if (mItemCount != mAdapter.getCount()) {
- throw new IllegalStateException("The content of the adapter has changed but "
- + "ListView did not receive a notification. Make sure the content of "
- + "your adapter is not modified from a background thread, but only "
- + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
- + ") with Adapter(" + mAdapter.getClass() + ")]");
- }
- setSelectedPositionInt(mNextSelectedPosition);
- // Pull all children into the RecycleBin.
- // These views will be reused if possible
- final int firstPosition = mFirstPosition;
- final RecycleBin recycleBin = mRecycler;
- // reset the focus restoration
- View focusLayoutRestoreDirectChild = null;
- // Don't put header or footer views into the Recycler. Those are
- // already cached in mHeaderViews;
- if (dataChanged) {
- for (int i = 0; i < childCount; i++) {
- recycleBin.addScrapView(getChildAt(i));
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(getChildAt(i),
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
- }
- }
- } else {
- recycleBin.fillActiveViews(childCount, firstPosition);
- }
- // take focus back to us temporarily to avoid the eventual
- // call to clear focus when removing the focused child below
- // from messing things up when ViewRoot assigns focus back
- // to someone else
- final View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- // TODO: in some cases focusedChild.getParent() == null
- // we can remember the focused view to restore after relayout if the
- // data hasn't changed, or if the focused position is a header or footer
- if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
- focusLayoutRestoreDirectChild = focusedChild;
- // remember the specific view that had focus
- focusLayoutRestoreView = findFocus();
- if (focusLayoutRestoreView != null) {
- // tell it we are going to mess with it
- focusLayoutRestoreView.onStartTemporaryDetach();
- }
- }
- requestFocus();
- }
- // Clear out old views
- detachAllViewsFromParent();
- switch (mLayoutMode) {
- case LAYOUT_SET_SELECTION:
- if (newSel != null) {
- sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
- } else {
- sel = fillFromMiddle(childrenTop, childrenBottom);
- }
- break;
- case LAYOUT_SYNC:
- sel = fillSpecific(mSyncPosition, mSpecificTop);
- break;
- case LAYOUT_FORCE_BOTTOM:
- sel = fillUp(mItemCount - 1, childrenBottom);
- adjustViewsUpOrDown();
- break;
- case LAYOUT_FORCE_TOP:
- mFirstPosition = 0;
- sel = fillFromTop(childrenTop);
- adjustViewsUpOrDown();
- break;
- case LAYOUT_SPECIFIC:
- sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
- break;
- case LAYOUT_MOVE_SELECTION:
- sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
- break;
- default:
- if (childCount == 0) {
- if (!mStackFromBottom) {
- final int position = lookForSelectablePosition(0, true);
- setSelectedPositionInt(position);
- sel = fillFromTop(childrenTop);
- } else {
- final int position = lookForSelectablePosition(mItemCount - 1, false);
- setSelectedPositionInt(position);
- sel = fillUp(mItemCount - 1, childrenBottom);
- }
- } else {
- if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
- sel = fillSpecific(mSelectedPosition,
- oldSel == null ? childrenTop : oldSel.getTop());
- } else if (mFirstPosition < mItemCount) {
- sel = fillSpecific(mFirstPosition,
- oldFirst == null ? childrenTop : oldFirst.getTop());
- } else {
- sel = fillSpecific(0, childrenTop);
- }
- }
- break;
- }
- // Flush any cached views that did not get reused above
- recycleBin.scrapActiveViews();
- if (sel != null) {
- // the current selected item should get focus if items
- // are focusable
- if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
- final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
- focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
- if (!focusWasTaken) {
- // selected item didn't take focus, fine, but still want
- // to make sure something else outside of the selected view
- // has focus
- final View focused = getFocusedChild();
- if (focused != null) {
- focused.clearFocus();
- }
- positionSelector(sel);
- } else {
- sel.setSelected(false);
- mSelectorRect.setEmpty();
- }
- } else {
- positionSelector(sel);
- }
- mSelectedTop = sel.getTop();
- } else {
- if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
- View child = getChildAt(mMotionPosition - mFirstPosition);
- if (child != null) positionSelector(child);
- } else {
- mSelectedTop = 0;
- mSelectorRect.setEmpty();
- }
- // even if there is not selected position, we may need to restore
- // focus (i.e. something focusable in touch mode)
- if (hasFocus() && focusLayoutRestoreView != null) {
- focusLayoutRestoreView.requestFocus();
- }
- }
- // tell focus view we are done mucking with it, if it is still in
- // our view hierarchy.
- if (focusLayoutRestoreView != null
- && focusLayoutRestoreView.getWindowToken() != null) {
- focusLayoutRestoreView.onFinishTemporaryDetach();
- }
- mLayoutMode = LAYOUT_NORMAL;
- mDataChanged = false;
- mNeedSync = false;
- setNextSelectedPositionInt(mSelectedPosition);
- updateScrollIndicators();
- if (mItemCount > 0) {
- checkSelectionChanged();
- }
- invokeOnItemScrollListener();
- } finally {
- if (!blockLayoutRequests) {
- mBlockLayoutRequests = false;
- }
- }
- }
- /**
- * Fills the list from pos down to the end of the list view.
- *
- * @param pos The first position to put in the list
- *
- * @param nextTop The location where the top of the item associated with pos
- * should be drawn
- *
- * @return The view that is currently selected, if it happens to be in the
- * range that we draw.
- */
- private View fillDown(int pos, int nextTop) {
- View selectedView = null;
- int end = (getBottom() - getTop()) - mListPadding.bottom;
- while (nextTop < end && pos < mItemCount) {
- // is this the selected item?
- boolean selected = pos == mSelectedPosition;
- View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
- nextTop = child.getBottom() + mDividerHeight;
- if (selected) {
- selectedView = child;
- }
- pos++;
- }
- return selectedView;
- }
- /**
- * Obtain the view and add it to our list of children. The view can be made
- * fresh, converted from an unused view, or used as is if it was in the
- * recycle bin.
- *
- * @param position Logical position in the list
- * @param y Top or bottom edge of the view to add
- * @param flow If flow is true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @return View that was added
- */
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
- // Try to use an exsiting view for this position
- child = mRecycler.getActiveView(position);
- if (child != null) {
- // Found it -- we're using an existing child
- // This just needs to be positioned
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
- // Make a new view for this position, or convert an unused view if possible
- child = obtainView(position, mIsScrap);
- // This needs to be positioned and measured
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。
那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:
- /**
- * Add a view as a child and make sure it is measured (if necessary) and
- * positioned properly.
- *
- * @param child The view to add
- * @param position The position of this child
- * @param y The y position relative to which this view will be positioned
- * @param flowDown If true, align top edge to y. If false, align bottom
- * edge to y.
- * @param childrenLeft Left edge where children should be positioned
- * @param selected Is this position selected?
- * @param recycled Has this view been pulled from the recycle bin? If so it
- * does not need to be remeasured.
- */
- private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
- boolean selected, boolean recycled) {
- final boolean isSelected = selected && shouldShowSelector();
- final boolean updateChildSelected = isSelected != child.isSelected();
- final int mode = mTouchMode;
- final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
- mMotionPosition == position;
- final boolean updateChildPressed = isPressed != child.isPressed();
- final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
- // Respect layout params that are already in the view. Otherwise make some up...
- // noinspection unchecked
- AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
- if (p == null) {
- p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT, 0);
- }
- p.viewType = mAdapter.getItemViewType(position);
- if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
- p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
- attachViewToParent(child, flowDown ? -1 : 0, p);
- } else {
- p.forceAdd = false;
- if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
- p.recycledHeaderFooter = true;
- }
- addViewInLayout(child, flowDown ? -1 : 0, p, true);
- }
- if (updateChildSelected) {
- child.setSelected(isSelected);
- }
- if (updateChildPressed) {
- child.setPressed(isPressed);
- }
- if (needToMeasure) {
- int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
- mListPadding.left + mListPadding.right, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- }
- child.measure(childWidthSpec, childHeightSpec);
- } else {
- cleanupLayoutState(child);
- }
- final int w = child.getMeasuredWidth();
- final int h = child.getMeasuredHeight();
- final int childTop = flowDown ? y : y - h;
- if (needToMeasure) {
- final int childRight = childrenLeft + w;
- final int childBottom = childTop + h;
- child.layout(childrenLeft, childTop, childRight, childBottom);
- } else {
- child.offsetLeftAndRight(childrenLeft - child.getLeft());
- child.offsetTopAndBottom(childTop - child.getTop());
- }
- if (mCachingStarted && !child.isDrawingCacheEnabled()) {
- child.setDrawingCacheEnabled(true);
- }
- }