ListView cache policy
ListView, like GridView, inherits from the AbsListView. The cache policy is implemented in the AbsListView class, so the cache policy is the same.
GetView (int, View, ViewGroup) method. A typical BaseAdapter implementation is as follows:
public class DemoAdapter extends BaseAdapter { private Context mContext; private List<Data> mDataList; public DemoAdapter(Context context, List<Data> dataList) { mContext = context; mDataList = dataList; } @Override public int getCount() { return mDataList.size(); } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_location_hot_city, null); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } final Data data = mDataList.get(position); viewHolder.tvTitle.setText(data.getName()); return convertView; } private static class ViewHolder { View itemView; TextView tvTitle; public ViewHolder(View view) { itemView = view; tvTitle = (TextView) view.findViewById(R.id.tv_title); }}}Copy the code
A very important optimization point in the above method is the judgment of convertView in the getView method. If it’s empty we need to create it; If it is not empty, it is a cached View that can be filled directly with data.
So how does ListView manage cached views and when does it call the baseAdapter.getView method and pass in cached views or null? So that’s the focus of this section, the whole discussion in this section is convertView.
1.1 RecycleBin
Before looking at the AbsListView and ListView code, the absListView.recyclebin class manages the reuse of views. RecycleBin has two levels of caching: ActiveViews and ScrapViews.
ActiveViews ScrapViews are old views that can be reused by Adapter to avoid unnecessary view creation. RecycleBin has the following fields:
/**
* 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;
private ArrayList<View> mSkippedScrap;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
Copy the code
The explanation is as follows:
-
MFirstActivePosition, mActiveViews
MFirstActivePosition refers to the position of the first View in mActiveViews in ListView. MActiveViews refers to the View being displayed on the screen. Save it before layout for reuse in layout. After layout, the remaining View that has not been reused will be degraded to scrap
-
MScrapViews, mViewTypeCount, and mCurrentScrap
MCurrentScrap = mScrapViews[0] And mViewTypeCount. MCurrentScrap = mScrapViews[0] But generally speaking
- When mViewTypeCount is 1, ScrapViews is mCurrentScrap
- When mViewTypeCount is greater than 1, ScrapViews is mScrapViews
-
MTransientStateViewsById, mTransientStateViews, mSkippedScrap
When view.hastransientState () is true, this data structure is used to store ScrapViews
When adapter.hasstableids () is true, mTransientStateViewsById is used for storage
When mDataChanged is false, mTransientStateViews is used for storage
Otherwise, mSkippedScrap is used. Views in the List will be cleared later and will not be reused
The RecycleBin method is essentially just a bunch of operations on the data. The main methods are:
-
setViewTypeCount(int)
Apply a List for each type of data element based on the incoming value to hold the cache
-
fillActiveViews(int childCount, int firstActivePosition)
Save the value of firstActivePosition and save views in the range [0, childCount) to the mActiveViews array
-
getActiveView(int)
In reverse, first subtract firstActivePosition from the passed argument to get the subscript of the View in the mActiveViews array, and then use the subscript to fetch the View
-
addScrapView(View, int)
Save old views to the corresponding ScrapViews, mTransientStateViewsById, mTransientStateViews, and mSkippedScrap collections
-
getScrapView(int)
Get old Views from the ScrapViews collection
-
getTransientStateView(int)
Get old Views from mTransientStateViewsById, mTransientStateViews collection
After understanding the RecycleBin important fields and methods, we can start to analyze the ListView cache mechanism. We use ListView initial layout, layout again and user slide three processes to analyze.
1.2 ListView initial layout
The initial layout of the ListView does not have any child views or cached views. First of all, the onMeasure method obviously does not require analysis because there is no cache design involved. So, let’s go straight to the onLayout method of the base class AbsListView.
/** * 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; final int childCount = getChildCount(); if (changed) { for (int i = 0; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } layoutChildren(); mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; // TODO: Move somewhere sane. This doesn't belong in onLayout(). if (mFastScroll ! = null) { mFastScroll.onItemCountChanged(getChildCount(), mItemCount); } mInLayout = false; }Copy the code
It doesn’t matter if changed is true in line 12 here, because there are no child views. Then line 19 is the layoutChildren() method, which is not implemented by the AbsListView but handed over to subclasses. So, let’s look at the listView.layoutChildren () method:
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
...
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. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [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;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
...
case LAYOUT_MOVE_SELECTION:
...
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();
// remove any header/footer that has been temp detached and not re-attached
removeUnusedFixedViews(mHeaderViewInfos);
removeUnusedFixedViews(mFooterViewInfos);
...
// Tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.dispatchFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
Copy the code
In the code above, you can get straight to the point and go to the if statement at 63. DataChanged will only be true if the data source changes, otherwise it will be false. So will execute line 68 recycleBin. FillActiveViews (childCount firstPosition) to cache the View from the ListView, but at present there is no child in the ListView View, So this line is not going to do anything for a while.
Next, the switch statement on line 75 is used. Normally, the ListView layout mode is LAYOUT_NORMAL, so the default branch is used. Since childCount is currently 0 and mStackFromBottom defaults to false, indicating that the layout defaults from top to bottom, the fillFromTop() method on line 84 is executed.
FillFromTop (int) calls fillDown(int, int) to fill the ListView from top to bottom until a screen of data is loaded or data is loaded:
/** * Fills the list from top to bottom, starting with mFirstPosition * * @param nextTop The location where the top of the first item should be * drawn * * @return The view that is currently selected */ private View fillFromTop(int nextTop) { mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } return fillDown(mFirstPosition, nextTop); } /** * 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 = (mBottom - mTop); if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= 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++; } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }Copy the code
In line 37 above, nextTop < end determines whether the screen has been exceeded, the nextTop value accumulates after each new view is added, and pos < mItemCount determines whether all data has been displayed. Line 40 makeAndAddView should be the focus of the fill process. Let’s look at how to make and add views.
/** * Obtains the view and adds 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 {@code true} to align top edge to y, {@code false} to align * bottom edge to y * @param childrenLeft left edge where children should be positioned * @param selected {@code true} if the position is selected, {@code false} * otherwise * @return the view that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { if (! mDataChanged) { // Try to use an existing view for this position. final View activeView = mRecycler.getActiveView(position); if (activeView ! = null) { // Found it. We're reusing an existing child, so it just needs // to be positioned like a scrap view. setupChild(activeView, position, y, flow, childrenLeft, selected, true); return activeView; } } // Make a new view for this position, or convert an unused view if // possible. final View child = obtainView(position, mIsScrap); // This needs to be positioned and measured. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }Copy the code
MDataChanged is obviously still false, So will try to call RecycleBin. GetActiveView get a layout the fill at the beginning of the view (the ListView. LayoutChildren () method of line 68 RecycleBin. FillActiveViews (child Count, firstPosition) will fill the view into the ActiveViews. So, what follows is the obtainView method on line 30 to create or reuse the View, followed by the setupChild method on line 32 to place and measure the View. So how does obtainView() actually work inside? Let’s take a look at this method:
/** * Gets a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view * is not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position the position to display * @param outMetadata an array of at least 1 boolean where the first entry * will be set {@code true} if the view is currently * attached to the window, {@code false} otherwise (e.g. * newly-inflated or remained scrap for multiple layout * passes) * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] outMetadata) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); outMetadata[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView ! = null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView ! = transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); if (scrapView ! = null) { if (child ! = scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); } else if (child.isTemporarilyDetached()) { outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } } if (mCacheColorHint ! = 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); }... setItemViewLayoutParams(child, position); . Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }Copy the code
The code in the obtainView() method contains very important logic, and this is the most important part of the ListView. First, outMetadata[0] is set to false, where outMetadata is actually the mIsScrap variable that will be used later. Then calls the RecycleBin. GetTransientStateView method to obtain the transient state of scrap the view. Obviously, there is no such view right now. Recyclebin. getScapView = scrapView; scrapView = scrapView; scrapView = scrapView; scrapView = scrapView; Finally, the Adapter.getView method is called to get a view.
Recall that a typical BaseAdapter implementation written in the ListView cache policy at the beginning of this article is that if the convertView passed in is NULL, we inflate a view and return it. The returned view is also returned as the result of the obtainView and eventually passed into the setupChild:
/** * Adds 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 {@code true} to align top edge to y, {@code false} to * align bottom edge to y * @param childrenLeft left edge where children should be positioned * @param selected {@code true} if the position is selected, {@code false} * otherwise * @param isAttachedToWindow {@code true} if the view is already attached * to the window, e.g. whether it was reused, or * {@code false} otherwise */ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean isAttachedToWindow) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem"); 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 = ! isAttachedToWindow || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); } p.viewType = mAdapter.getItemViewType(position); p.isEnabled = mAdapter.isEnabled(position); // Set up view state before attaching the view, since we may need to // rely on the jumpDrawablesToCurrentState() call that occurs as part // of view attachment. if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (mChoiceMode ! = CHOICE_MODE_NONE && mCheckStates ! = null) { if (child instanceof Checkable) { ((Checkable) child).setChecked(mCheckStates.get(position)); } else if (getContext().getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { child.setActivated(mCheckStates.get(position)); } } if ((isAttachedToWindow && ! p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); // If the view was previously attached for a different position, // then manually jump the drawables. if (isAttachedToWindow && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition) ! = position) { child.jumpDrawablesToCurrentState(); } } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); // add view in layout will reset the RTL properties. We have to re-resolve them child.resolveRtlPropertiesIfNeeded(); } if (needToMeasure) { final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); final int lpHeight = p.height; final int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), 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); } Trace.traceEnd(Trace.TRACE_TAG_VIEW); }Copy the code
In the setupChild method, isAttachedToWindow passes mIsScrap[0], which is set to false in the obtainView method. So, if we skip some of the state checks, we can go straight to line 59 of the if statement. Obviously, none of the conditions are met, so we go to line 70 of the else branch. In the branch will call ViewGroup. AddViewInLayout method to add the child to the ListView. Also, because isAttachedToWindow is false, needToMeasure is initialized to true. Therefore, the child completes the measure operation at line 91 and the Layout operation at line 103.
It is worth mentioning, after layers of returns to the layoutChildren method, method will continue to line 105 recycleBin. ScrapActiveViews (); Statement, which moves all remaining ActionViews to the ScrapView collection. In other words, the ActionViews life cycle only exists in the layout process.
At this point, the ListView initial layout cache process has been explored. Let’s look at the caching process when we rearrange the layout.
1.3 The ListView layout again
Layout again is mainly to explore the case of cache, ListView how to handle cache.
We start directly with the layoutChildren method:
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
...
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. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [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;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
...
case LAYOUT_MOVE_SELECTION:
...
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();
// remove any header/footer that has been temp detached and not re-attached
removeUnusedFixedViews(mHeaderViewInfos);
removeUnusedFixedViews(mFooterViewInfos);
...
// Tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.dispatchFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
Copy the code
Or like first layout, executes line 68 recycleBin. FillActiveViews method, but due to the ListView in have a View at this time, so the child View will be cached in the recycleBin. Then execute the detachAllViewsFromParent method on line 72 to set the children’s mParent and themselves to null, detach temporarily from the ListView. Call the attachViewToParent method to attach again later during reuse. Then we go to switch on line 75, and again we enter the default branch. And since childCount is not zero, it’s going to branch else. The default mSelectedPosition is INVALID_POSITION -1, so the fillSpecific operation is performed on line 94.
/** * Put a specific item at a specific location on the screen and then build * up and down from there. * * @param position The reference view to use as the starting point * @param top Pixel offset from the top of this view to the top of the * reference view. * * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific(int position, int top) { boolean tempIsSelected = position == mSelectedPosition; View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = position; View above; View below; final int dividerHeight = mDividerHeight; if (! mStackFromBottom) { above = fillUp(position - 1, temp.getTop() - dividerHeight); // This will correct for the top of the first view not touching the top of the list adjustViewsUpOrDown(); below = fillDown(position + 1, temp.getBottom() + dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooHigh(childCount); } } else { below = fillDown(position + 1, temp.getBottom() + dividerHeight); // This will correct for the bottom of the last view not touching the bottom of the list adjustViewsUpOrDown(); above = fillUp(position - 1, temp.getTop() - dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooLow(childCount); } } if (tempIsSelected) { return temp; } else if (above ! = null) { return above; } else { return below; }}Copy the code
The fillSpecific() method is similar to the fillUp() and fillDown() methods, which use fill, except that the fillSpecific() method first loads the child View at the specified location onto the screen. And then fill the other child views up and down from that View.
Here we’ll focus directly on the key method — makeAndAddView:
/** * Obtains the view and adds 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 {@code true} to align top edge to y, {@code false} to align * bottom edge to y * @param childrenLeft left edge where children should be positioned * @param selected {@code true} if the position is selected, {@code false} * otherwise * @return the view that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { if (! mDataChanged) { // Try to use an existing view for this position. final View activeView = mRecycler.getActiveView(position); if (activeView ! = null) { // Found it. We're reusing an existing child, so it just needs // to be positioned like a scrap view. setupChild(activeView, position, y, flow, childrenLeft, selected, true); return activeView; } } // Make a new view for this position, or convert an unused view if // possible. final View child = obtainView(position, mIsScrap); // This needs to be positioned and measured. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }Copy the code
Try to get the View from ActiveViews first. It then calls the setupChild method on line 23 and returns the actionView. In this case, the ListView does not execute the obtainView method on line 30 below, because the View obtained in ActiveViews must have been displayed on the screen until the moment before, without the need for the Adapter to be reconfigured or the UI value to be updated.
Note that in the setupChild method on line 23 above, the last argument is true, which is isAttachedToWindow. Therefore, needToMeasure is false, indicating that the child does not need to be remeasured or layout. Also, this causes if to be true on line 59 below, which causes the attachViewToParent method to be called, reattaching the child to the ListView:
/** * Adds 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 {@code true} to align top edge to y, {@code false} to * align bottom edge to y * @param childrenLeft left edge where children should be positioned * @param selected {@code true} if the position is selected, {@code false} * otherwise * @param isAttachedToWindow {@code true} if the view is already attached * to the window, e.g. whether it was reused, or * {@code false} otherwise */ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean isAttachedToWindow) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem"); 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 = ! isAttachedToWindow || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); } p.viewType = mAdapter.getItemViewType(position); p.isEnabled = mAdapter.isEnabled(position); // Set up view state before attaching the view, since we may need to // rely on the jumpDrawablesToCurrentState() call that occurs as part // of view attachment. if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (mChoiceMode ! = CHOICE_MODE_NONE && mCheckStates ! = null) { if (child instanceof Checkable) { ((Checkable) child).setChecked(mCheckStates.get(position)); } else if (getContext().getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { child.setActivated(mCheckStates.get(position)); } } if ((isAttachedToWindow && ! p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); // If the view was previously attached for a different position, // then manually jump the drawables. if (isAttachedToWindow && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition) ! = position) { child.jumpDrawablesToCurrentState(); } } else { p.forceAdd = false; if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); // add view in layout will reset the RTL properties. We have to re-resolve them child.resolveRtlPropertiesIfNeeded(); } if (needToMeasure) { final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); final int lpHeight = p.height; final int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), 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); } Trace.traceEnd(Trace.TRACE_TAG_VIEW); }Copy the code
Recall the meaning of if on line 59
- No cache is available for the first layout, so a new View is created and isAttachedToWindow is set to false, which adds a new View to the ListView by calling the addViewInLayout method
- The second layout is temporarily in detach state because there is a cache available and the cache View is temporarily in detach state because the detachAllViewsFromParent() method is called. After successfully reusing the View, the setupChild method is called with isAttachedToWindow as true, and attachViewToParent is executed, restoring the child to attach state
Now that the second layout process is over, let’s look at what happens when the user slides the ListView.
1.4 User Sliding
The onTouchEvent implementation of the ListView is in the AbsListView, which means that the GridView has the same mechanism.
@Override public boolean onTouchEvent(MotionEvent ev) { if (! isEnabled()) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isClickable() || isLongClickable(); } if (mPositionScroller ! = null) { mPositionScroller.stop(); } if (mIsDetaching || ! isAttachedToWindow()) { // Something isn't right. // Since we rely on being attached to get data set change notifications, // don't risk doing anything where we might try to resync and find things // in a bogus state. return false; } startNestedScroll(SCROLL_AXIS_VERTICAL); if (mFastScroll ! = null && mFastScroll.onTouchEvent(ev)) { return true; } initVelocityTrackerIfNotExists(); final MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { onTouchDown(ev); break; } case MotionEvent.ACTION_MOVE: { onTouchMove(ev, vtev); break; } case MotionEvent.ACTION_UP: { onTouchUp(ev); break; } case MotionEvent.ACTION_CANCEL: { onTouchCancel(); break; } case MotionEvent.ACTION_POINTER_UP: { onSecondaryPointerUp(ev); final int x = mMotionX; final int y = mMotionY; final int motionPosition = pointToPosition(x, y); if (motionPosition >= 0) { // Remember where the motion event started final View child = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = child.getTop(); mMotionPosition = motionPosition; } mLastY = y; break; } case MotionEvent.ACTION_POINTER_DOWN: { // New pointers take over dragging duties final int index = ev.getActionIndex(); final int id = ev.getPointerId(index); final int x = (int) ev.getX(index); final int y = (int) ev.getY(index); mMotionCorrection = 0; mActivePointerId = id; mMotionX = x; mMotionY = y; final int motionPosition = pointToPosition(x, y); if (motionPosition >= 0) { // Remember where the motion event started final View child = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = child.getTop(); mMotionPosition = motionPosition; } mLastY = y; break; } } if (mVelocityTracker ! = null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }Copy the code
MotionEvent has too many types of events, and we only care about moving events, so just look at the onTouchMove method:
private void onTouchMove(MotionEvent ev, MotionEvent vtev) { if (mHasPerformedLongPress) { // Consume all move events following a successful long press. return; } int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { pointerIndex = 0; mActivePointerId = ev.getPointerId(pointerIndex); } if (mDataChanged) { // Re-sync everything if data has been changed // since the scroll operation can query the adapter. layoutChildren(); } final int y = (int) ev.getY(pointerIndex); switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: // Check if we have moved far enough that it looks more like a // scroll than a tap. If so, we'll enter scrolling mode. if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) { break; } // Otherwise, check containment within list bounds. If we're // outside bounds, cancel any active presses. final View motionView = getChildAt(mMotionPosition - mFirstPosition); final float x = ev.getX(pointerIndex); if (! pointInView(x, y, mTouchSlop)) { setPressed(false); if (motionView ! = null) { motionView.setPressed(false); } removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); mTouchMode = TOUCH_MODE_DONE_WAITING; updateSelectorState(); } else if (motionView ! = null) { // Still within bounds, update the hotspot. final float[] point = mTmpPoint; point[0] = x; point[1] = y; transformPointToViewLocal(point, motionView); motionView.drawableHotspotChanged(point[0], point[1]); } break; case TOUCH_MODE_SCROLL: case TOUCH_MODE_OVERSCROLL: scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev); break; }}Copy the code
The onTouchMove method does a switch on mTouchMode, which is TOUCH_MODE_SCROLL when a finger is swiped on the screen, so we go straight to scrollIfNeeded. Because the method is a bit too long, I have omitted some irrelevant code:
private void scrollIfNeeded(int x, int y, MotionEvent vtev) { int rawDeltaY = y - mMotionY; int scrollOffsetCorrection = 0; int scrollConsumedCorrection = 0; if (mLastY == Integer.MIN_VALUE) { rawDeltaY -= mMotionCorrection; }... final int deltaY = rawDeltaY; int incrementalDeltaY = mLastY ! = Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY; int lastYCorrection = 0; if (mTouchMode == TOUCH_MODE_SCROLL) { if (PROFILE_SCROLLING) { if (! mScrollProfilingStarted) { Debug.startMethodTracing("AbsListViewScroll"); mScrollProfilingStarted = true; } } if (mScrollStrictSpan == null) { // If it's non-null, we're already in a scroll. mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll"); } if (y ! = mLastY) { // We may be here after stopping a fling and continuing to scroll. // If so, we haven't disallowed intercepting touch events yet. // Make sure that we do so in case we're in a parent that can intercept. if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 && Math.abs(rawDeltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent ! = null) { parent.requestDisallowInterceptTouchEvent(true); } } final int motionIndex; if (mMotionPosition >= 0) { motionIndex = mMotionPosition - mFirstPosition; } else { // If we don't have a motion position that we can reliably track, // pick something in the middle to make a best guess at things below. motionIndex = getChildCount() / 2; } int motionViewPrevTop = 0; View motionView = this.getChildAt(motionIndex); if (motionView ! = null) { motionViewPrevTop = motionView.getTop(); } // No need to do all this work if we're not going to move anyway boolean atEdge = false; if (incrementalDeltaY ! = 0) { atEdge = trackMotionScroll(deltaY, incrementalDeltaY); } // Check to see if we have bumped into the scroll limit motionView = this.getChildAt(motionIndex); if (motionView ! = null) { // Check if the top of the motion view is where it is // supposed to be final int motionViewRealTop = motionView.getTop(); if (atEdge) { // Apply overscroll int overscroll = -incrementalDeltaY - (motionViewRealTop - motionViewPrevTop); if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll, mScrollOffset)) { lastYCorrection -= mScrollOffset[1]; if (vtev ! = null) { vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } } else { final boolean atOverscrollEdge = overScrollBy(0, overscroll, 0, mScrollY, 0, 0, 0, mOverscrollDistance, true); if (atOverscrollEdge && mVelocityTracker ! = null) { // Don't allow overfling if we're at the edge mVelocityTracker.clear(); } final int overscrollMode = getOverScrollMode(); if (overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && ! contentFits())) { if (! atOverscrollEdge) { mDirection = 0; // Reset when entering overscroll. mTouchMode = TOUCH_MODE_OVERSCROLL; } if (incrementalDeltaY > 0) { mEdgeGlowTop.onPull((float) -overscroll / getHeight(), (float) x / getWidth()); if (! mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } invalidateTopGlow(); } else if (incrementalDeltaY < 0) { mEdgeGlowBottom.onPull((float) overscroll / getHeight(), 1.f - (float) x / getWidth()); if (! mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } invalidateBottomGlow(); } } } } mMotionY = y + lastYCorrection + scrollOffsetCorrection; } mLastY = y + lastYCorrection + scrollOffsetCorrection; } } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { ... }}Copy the code
The trackMotionScroll method on line 57 is the one above, which will be called many times because it is triggered by every event of the slide. TrackMotionScroll code is as follows:
/** * Track a motion scroll * * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion * began. Positive numbers mean the user's finger is moving down the screen. * @param incrementalDeltaY Change in deltaY from the previous event. * @return true if we're already at the beginning/end of the list and have nothing to do. */ boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { final int childCount = getChildCount(); if (childCount == 0) { return true; } final int firstTop = getChildAt(0).getTop(); final int lastBottom = getChildAt(childCount - 1).getBottom(); final Rect listPadding = mListPadding; // "effective padding" In this case is the amount of padding that affects // how much space should not be filled by items. If we don't clip to padding // there is no effective padding. int effectivePaddingTop = 0; int effectivePaddingBottom = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { effectivePaddingTop = listPadding.top; effectivePaddingBottom = listPadding.bottom; } // FIXME account for grid vertical spacing too? final int spaceAbove = effectivePaddingTop - firstTop; final int end = getHeight() - effectivePaddingBottom; final int spaceBelow = lastBottom - end; final int height = getHeight() - mPaddingBottom - mPaddingTop; if (deltaY < 0) { deltaY = Math.max(-(height - 1), deltaY); } else { deltaY = Math.min(height - 1, deltaY); } if (incrementalDeltaY < 0) { incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); } else { incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); } final int firstPosition = mFirstPosition; // Update our guesses for where the first and last views are if (firstPosition == 0) { mFirstPositionDistanceGuess = firstTop - listPadding.top; } else { mFirstPositionDistanceGuess += incrementalDeltaY; } if (firstPosition + childCount == mItemCount) { mLastPositionDistanceGuess = lastBottom + listPadding.bottom; } else { mLastPositionDistanceGuess += incrementalDeltaY; } final boolean cannotScrollDown = (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0); final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0); if (cannotScrollDown || cannotScrollUp) { return incrementalDeltaY ! = 0; } final boolean down = incrementalDeltaY < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 0; if (down) { int top = -incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { top += listPadding.top; } for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } } } else { int bottom = getHeight() - incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { bottom -= listPadding.bottom; } for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } } } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; mBlockLayoutRequests = true; if (count > 0) { detachViewsFromParent(start, count); mRecycler.removeSkippedScrap(); } // invalidate before moving the children to avoid unnecessary invalidate // calls to bubble up from the children all the way to the top if (! awakenScrollBars()) { invalidate(); } offsetChildrenTopAndBottom(incrementalDeltaY); if (down) { mFirstPosition += count; } final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } mRecycler.fullyDetachScrapViews(); if (! inTouchMode && mSelectedPosition ! = INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(mSelectedPosition, getChildAt(childIndex)); } } else if (mSelectorPosition ! = INVALID_POSITION) { final int childIndex = mSelectorPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(INVALID_POSITION, getChildAt(childIndex)); } } else { mSelectorRect.setEmpty(); } mBlockLayoutRequests = false; invokeOnItemScrollListener(); return false; }Copy the code
This method takes two parameters, deltaY representing the distance from the position of the finger when it was pressed to the current position of the finger, incrementalDeltaY representing the change in the position of the finger in the Y direction since the last time the Event was triggered, We can tell if the user is sliding up or down by checking the positive or negative value of incrementalDeltaY. As shown in line 71, if incrementalDeltaY is less than 0, it is sliding down, otherwise it is sliding up.
As you can see, starting at line 89, when the ListView slides down, it enters a for loop, fetching sub-views from the top. In line 91, if the sub-view’s bottom value is less than the top value, Recyclebin.addscrapview () adds the View to the ScrapView and increments the count counter, which counts how many of the views are removed from the screen. So if you slide the ListView up, the process is basically the same, except that you get the child View from the bottom up, and then you determine if the child View’s top value is greater than the bottom value, and if it is greater than the View has moved off the screen, you add it to the deprecated cache as well, And increment the counter by one.
Then, in line 132, a detach operation is performed based on the current counter value, which detach all child views that are removed from the screen. Then in line 142 calls the offsetChildrenTopAndBottom () method, and the incrementalDeltaY passed as a parameter, the purpose of this method is to allow all children in the ListView View are carried out in accordance with the incoming parameter value of the corresponding offset, This allows the contents of the ListView to scroll as you drag them.
Then line 149 checks that if the bottom of the last View in the ListView has moved into the screen, or the top of the first View in the ListView has moved into the screen, the fillGap() method is called, So we can guess that the fillGap() method is used to load off-screen data. The listView.fillgap method is abstract and needs to be subclassed, so let’s look at the listView.fillgap method:
/** * {@inheritDoc} */ @Override void fillGap(boolean down) { final int count = getChildCount(); if (down) { int paddingTop = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { paddingTop = getListPaddingTop(); } final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : paddingTop; fillDown(mFirstPosition + count, startOffset); correctTooHigh(getChildCount()); } else { int paddingBottom = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { paddingBottom = getListPaddingBottom(); } final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - paddingBottom; fillUp(mFirstPosition - 1, startOffset); correctTooLow(getChildCount()); }}Copy the code
In both fillDown and fillUp, makeAndAddView is used to get the View, but in this case, the ActiveViews method is used to get the View because the ActiveViews are already empty after the layout process is complete. ObtainView is used to get the View:
/** * Obtains the view and adds 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 {@code true} to align top edge to y, {@code false} to align * bottom edge to y * @param childrenLeft left edge where children should be positioned * @param selected {@code true} if the position is selected, {@code false} * otherwise * @return the view that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { if (! mDataChanged) { // Try to use an existing view for this position. final View activeView = mRecycler.getActiveView(position); if (activeView ! = null) { // Found it. We're reusing an existing child, so it just needs // to be positioned like a scrap view. setupChild(activeView, position, y, flow, childrenLeft, selected, true); return activeView; } } // Make a new view for this position, or convert an unused view if // possible. final View child = obtainView(position, mIsScrap); // This needs to be positioned and measured. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; } /** * Gets a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view * is not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position the position to display * @param outMetadata an array of at least 1 boolean where the first entry * will be set {@code true} if the view is currently * attached to the window, {@code false} otherwise (e.g. * newly-inflated or remained scrap for multiple layout * passes) * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] outMetadata) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); outMetadata[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView ! = null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView ! = transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); if (scrapView ! = null) { if (child ! = scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); } else if (child.isTemporarilyDetached()) { outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } } if (mCacheColorHint ! = 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); }... setItemViewLayoutParams(child, position); . Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }Copy the code
Above the 61th guild calls RecycleBin. GetTransientStateView method for transient state ScrapView, general is not. So recyclebin. getScrapView on line 83 gets a View from ScrapViews. In the trackMotionScroll method we add any View that moves off the screen to ScrapViews, so it’s definitely accessible. Therefore, the adapter.getView method is called on line 84 to make the Adapter reuse the View and populate it with the corresponding data so that the View looks as if it were brand new.
Of course, if the reuse of View, the control did not handle well, there will be reuse caused by the bug, it is worth noting. A typical problem is setting the ImageView to display an image if the value is true, but nothing is done when the value is false, and a quick swipe will cause the ImageView to display a different image from the actual data.
So that’s all there is to the ListView caching mechanism.