Of all Android's commonly used native controls, ListView is probably the most complex one. It is designed to deal with a lot of content elements, and the mobile screen can't show all the content. ListView can display content in the form of a list, and content beyond the screen can be moved to the screen by simply sliding through the fingers.
In addition, ListView has a very magical function, I believe you should all experience that even if the ListView loads very much data, such as hundreds of thousands or more, ListView will not have OOM or crash, and as our fingers slide to browse more data, the memory occupied by the program will not grow. So how does ListView achieve such amazing functionality?
First, let's look at the inheritance structure of ListView, as shown in the following figure:
As you can see, the inheritance structure of ListView is quite complex. It is directly inherited from AbsListView. AbsListView has two sub-implementation classes, one is ListView and the other is GridView. So we can guess from this point that ListView and GridView have a lot in common in working principle and implementation. Then AbsListView inherits from AdapterView, and AdapterView inherits from ViewGroup, which we are familiar with. A look at the inheritance structure of ListView will help us to analyze the code more clearly later.
The Role of Adapter
Adapter believes that everyone is familiar with ListView, and we will use it when we use ListView. So, have you ever thought about it carefully, why do you need the adapter? It always feels like the use of ListView is much more complicated than other controls because of the Adapter. So let's first learn what role Adapter plays.
In fact, in the final analysis, the control is used to interact and display data, but ListView is more special, it is used to show a lot of data, but ListView only undertakes interaction and display work, as to where the data comes from, ListView does not care. Therefore, the most basic working mode we can imagine for ListView is to have a ListView control and a data source.
But if you really let ListView work directly with data sources, the adaptation of ListView is very complicated. Because the concept of data source is too vague, we only know that it contains a lot of data. As for the type of data source, there is no strict definition. It may be an array, a collection, or even a cursor queried in a database table. So if ListView really adapts for each data source, one is that the scalability is poor, and there are only a few adapts built-in, which cannot be added dynamically. Second, it goes beyond the scope of work it should be responsible for, instead of just assuming interaction and presentation work, ListView will become bloated.
So obviously the Android development team won't allow this to happen, so there's a mechanism like Adapter. As the name implies, Adapter is an adapter. It acts as a bridge between ListView and data sources. ListView does not directly deal with data sources, but uses the bridge of Adapter to access real data sources. Unlike before, the interface of Adapter is unified, so ListView does not need to worry about any adaptation issues. Adapter is also an interface, which can implement a variety of subclasses. Each subclass can complete specific functions through its own logic, and adapt to specific data sources. For example, Array Adapter can be used to adapt data sources of array and List type, SimpleCursor Adapter can be used to adapt data sources of cursor type, which is very important. It skillfully solves the difficult problem of adapting data sources, and has quite good scalability. The simple schematic diagram is as follows:
Of course, the role of Adapter is not only data source adaptation, but also a very important method that we need to rewrite in the Adapter, which is the getView() method, which will be discussed in detail in the following article.
RecycleBin mechanism
So before we start to analyze the source code of ListView, there is another thing we need to know in advance, namely the RecycleBin mechanism, which is also the most important reason why ListView can achieve hundreds of thousands of data without OOM. In fact, RecycleBin does not have much code, only about 300 lines. It is an internal class written in AbsListView, so all the subclasses inherited from AbsListView, namely ListView and GridView, can use this mechanism. Let's look at the main code in RecycleBin, as follows:
/**
* 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.
*/
//Storage View
private View[] mActiveViews = new View[0];
/**
* Unsorted views that can be used by the adapter as a convert view.
*/
//Two lists, mScrapViews and mCurrentScrap, store abandoned Views
private ArrayList<View>[] mScrapViews;
private int mViewTypeCount;
//Two lists, mScrapViews and mCurrentScrap, store abandoned Views
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
*/
//The first parameter represents the number of view s to store, and the second parameter represents the position value of the first visible element in the ListView.
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
*/
//Used to retrieve data from the mActiveViews array. This method receives a position parameter that represents the location of the element in the ListView, and automatically converts the position value into the subscript value corresponding to the mActiveViews array within the method. It should be noted that the View stored in mActiveViews will be removed from mActiveViews once it is acquired. The next time you get the same View, it will return null, which means mActiveViews can not be reused.
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
*/
//This method receives a View parameter to cache an abandoned View. When a View is determined to be abandoned (such as scrolling out of the screen), it should be called to cache the View. In RecycleBin, mScrapViews and mCurrentScrap are used to store the abandoned View.
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.
*/
//getScrapView is used to extract a View from the discarded cache
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;
}
//Enable a RecycleBin caching mechanism for each type of data item
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;
}
}
The RecycleBin code here is incomplete, and I just mention the main methods. So let's start with a simple interpretation of these methods, which will be very helpful for later analysis of the working principle of ListView.
The fillActiveViews() method takes two parameters. The first parameter represents the number of views to be stored, and the second parameter represents the position value of the first visible element in ListView. The mActiveViews array is used in RecycleBin to store Views, and when called, the specified elements in the ListView are stored in the mActiveViews array based on the parameters passed in.
The getActiveView() method corresponds to fillActiveViews(), which is used to retrieve data from the mActiveViews array. This method receives a position parameter that represents the location of the element in the ListView, and automatically converts the position value into the subscript value corresponding to the mActiveViews array within the method. It should be noted that the View stored in mActiveViews will be removed from mActiveViews once it is acquired. The next time you get the same View, it will return null, which means mActiveViews can not be reused.
AddiScrapView () is used to cache an abandoned View. This method receives a View parameter. When a View is determined to be abandoned (such as scrolling out of the screen), it should be called to cache the View. In RecycleBin, mScrapViews and mCurrentScrap are used to store the abandoned View.
GetScrapView is used to extract a View from the discarded cache. The Views in these discarded caches are not in order, so the algorithm in the getScrapView() method is very simple, that is, to retrieve a scrap view at the tail directly from mCurrentScrap and return it.
setViewTypeCount() We all know that in the Adapter, we can rewrite a getViewTypeCount() to represent several types of data items in ListView, and the setViewTypeCount() method is designed to enable a RecycleBin caching mechanism for each type of data item. In fact, the getViewTypeCount() method is not usually used very much, so we just need to know that RecycleBin has such a function.
After understanding the main methods in RecycleBin and their usefulness, I can start to analyze the working principle of ListView. Here I will continue to follow the previous way of analyzing source code, i.e. follow the main line execution process to gradually read and stop, otherwise if all the code in ListView is posted, this article will be very long.
First Layout
In any case, ListView is ultimately inherited from View even if it is special, so its execution process will follow the View rules.
The execution process of View can be divided into three steps: onMeasure() for measuring the size of View, onLayout() for determining the layout of View, and onDraw() for drawing View onto the interface. In ListView, onMeasure() is nothing special, because it is ultimately a View, occupying the most space and usually the entire screen. onDraw() has no meaning in ListView, because ListView itself is not responsible for drawing, but is drawn by the child elements in ListView. So most of the magic of ListView is actually done in the onLayout() method, so this article is also the main content of this method.
If you look in the ListView source code, you will find that there is no onLayout() method in ListView, because this method is implemented in AbsListView, the parent of ListView. The code is as follows:
/**
* 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;
}
As you can see, the onLayout() method does not do any complicated logical operations, mainly a judgment. If the size or location of the ListView changes, then the changed variable becomes true, which requires that all sublayouts be forced to redraw. There's nothing else that's hard to understand, but we noticed that the layoutChildren() method was called on line 16, and we can guess from the method name that this method is used to lay out the child elements, but when you get into this method, you'll find that it's an empty method without a single line of code. This is understandable, of course, because the layout of child elements should be done by specific implementation classes, not by parent classes. Then enter the layoutChildren() method of ListView, the code is as follows:
@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;
}
}
}
This code is rather long, let's focus on it. First of all, we can confirm that there is no sub-View in ListView. Data is managed by Adapter and not displayed on the interface. Therefore, the value obtained by the getChildCount() method in line 19 must be 0. In line 81, the execution logic is judged by the Boolean value of dataChanged. DataChanged becomes true only when the data source changes. Others are false. Therefore, the execution logic of line 90 is entered and the fillActiveViews() method of RecycleBin is invoked. In principle, the fillActiveViews() method is called to cache the sub-Views of ListView, but there are no sub-Views in ListView at present, so this line does not work for the time being.
Next, in line 114, the layout mode is determined by the value of mLayoutMode, which by default is the normal mode LAYOUT_NORMAL, so it goes into the default statement in line 140. The next two if judgments will be made immediately. The child Count is currently equal to 0, and the default layout order is from top to bottom, so it will go to the fillFromTop() method in line 145. Let's follow up and see:
/**
* 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);
}
As you can see from the annotations of this method, its main task is to fill ListView from top to bottom, starting with mFirstPosition. There is no logic in this method itself, that is, to judge the validity of the mFirstPosition value and then call the fillDown() method, then we have reason to guess that filling ListView is done in the fillDown() method. Enter the fillDown() method with the following code:
/**
* 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;
}
As you can see, a while loop is used to perform repetitive logic. At first, the value of nextTop is the pixel value from the top of the first child element to the top of the entire ListView, pos is the value of the mFirstPosition just passed in, and end is the pixel value from the bottom of the ListView minus the top, and mItemCount is the number of elements in the Adapter. So in the beginning, nextTop must be less than the end value, and pos is also less than the mItemCount value. Then every time a while loop is executed, the value of pos is added by 1, and the value of nextTop is increased. When nextTop is greater than or equal to end, that is, when the child element has exceeded the current screen, or pos is greater than or equal to mItemCount, that is, when all the elements in the adapter are finished traversing, the while loop will jump out.
So what's going on in the while loop? It's worth noting that line 18 calls the makeAndAddView() method, which goes into this method, and the code is as follows:
/**
* 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;
}
In line 19, we try to get an active view quickly from RecycleBin, but unfortunately there is no view cached in RecycleBin, so the value here must be null. After null is obtained, it will continue to run downward. At line 28, it will call the obtainView() method to try to get a View again. This time, the obtainView() method can guarantee that a View will be returned, so the obtained View will be immediately passed into the setupChild() method. So how does obtainView() work internally? Let's go into this method first and see:
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
The obtainView() method doesn't have much code, but it contains very important logic. It's probably the most important part of the entire ListView, not to be exaggerated. So let's follow the execution process and call RecycleBin's getScrapView() method in line 19 to try to get a view from an obsolete cache. In the same way, it's certainly not available here. The getScrapView() method returns a null. What should we do then? It doesn't matter. The code goes to line 33 and calls the mAdapter's getView() method to get a View. So what is mAdapter? Of course, it's the adapter associated with the current ListView. And what about the getView() method? Needless to say, this is one of the most frequently rewritten methods when we use ListView in our daily life. Here, the getView() method passes in three parameters, position, null and this.
So how do we usually write the getView() method when we write the Adapter of ListView? Let me give you a simple example:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
The getView() method accepts three parameters. The first parameter position represents the position of the current child element. We can get the data related to it through the specific location. The second parameter, convertView, just passed in is null, indicating that no convertView can be used, so we will call the inflate() method of LayoutInflater to load a layout. Next, some properties and values will be set for the view, and finally the view will be returned.
Then the View will be returned as the result of obtainView() and eventually passed into the setupChild() method. In fact, in the first layout process, all the sub-views are loaded by calling the inflate() method of LayoutInflater, which will be relatively time-consuming, but don't worry, this will not happen again. So let's go on to see:
/**
* 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);
}
}
There is a lot of code in the setupChild() method, but if we only look at the core code, it's very simple. We just called the obtainView() method to get the child element View, and here in line 40 we call the addViewInLayout() method to add it to ListView. Then, according to the while loop in fillDown() method, the sub-element View fills up the entire ListView control and then jumps out. That is to say, even if there are 1000 pieces of data in our Adapter, the ListView will only load the data on the first screen. The remaining data can't be seen on the screen at present, so it won't do any extra loading work, so as to ensure that the ListView can be loaded. Content can be quickly displayed on the screen.
So far, the first Layout process is over.
Second Layout
Although I haven't found the specific reason in the source code, if you do your own experiment, you will find that even a simple View will go through at least two onMeasure() and two onLayout() processes before it is displayed on the interface. In fact, this is only a small detail, usually we do not have much impact, because whether it is onMeasure() or onLayout() several times, anyway, it is the same logic to implement, we do not need to care too much. But this is different in ListView, because it means that the layoutChildren() process will be executed twice, which involves adding child elements to ListView. If the same logic is executed twice, there will be a duplicate data in ListView. So ListView has done the second Layout logical processing in the layoutChildren() process, which has solved this problem very skillfully. Now let's analyze the second Layout process.
In fact, the basic process of the second Layout is similar to that of the first Layout, so we start with the layoutChildren() method:
@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;
}
}
}
Also in line 19, we call getChildCount() to get the number of sub-views, but the value won't be zero anymore, but the number of sub-views that can be displayed on a screen in ListView, because we just added so many sub-views to ListView in the first Layout process. The fillActiveViews() method of RecycleBin is called in line 90 below, but this time the effect is different because there are already child Views in ListView so that all child Views are cached into the mActiveViews array of RecycleBin, which will be used later.
Next is a very important operation, calling the detachAllViewsFromParent() method on line 113. This method wipes out all the subviews in the ListView to ensure that the second Layout process does not produce a duplicate data. Some friends may ask, so that the loaded View has been removed, and will be reloaded again later, this is not a serious impact on efficiency? Don't worry, remember that we just invoked RecycleBin's fillActiveViews() method to cache sub-Views. We'll use these cached Views to load them directly later, instead of re-executing the inflate process, so there won't be any significant impact on efficiency.
So let's go on to see that in line 141 of the judgment logic, because it's no longer equal to zero, it goes into the else statement. There are three logical judgments in the else statement. The first one is not valid because by default we have not selected any child elements. mSelectedPosition should be equal to -1. The second logical judgment is usually valid because the value of mFirstPosition is initially equal to zero, as long as the data in adapter is greater than zero. Enter the fillSpecific() method, and the code is as follows:
/**
* 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;
}
}
FilSpecific () is a new method, but in fact it has the same function as fillUp() and fillDown() methods. The main difference is that the fillSpecific() method first loads the child View of the specified position onto the screen, then loads the child View up and down. So since the position we pass in here is the location of the first sub-View, the fillSpecific() method is basically the same as the fillDown() method. Instead of focusing too much on its details, we focus on the makeAndAddView() method. Back to the makeAndAddView() method again, the code is as follows:
/**
* 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;
}
Still try to get Active View from RecycleBin in line 19, but this time it must be, because we called RecycleBin's fillActiveViews() method to cache the sub-View. So, instead of going into the obtainView() method on line 28, you go directly into the setupChild() method, which saves a lot of time because if you go to the infalte layout again in the obtainView() method, the initial loading efficiency of ListView will be greatly reduced.
Note that in line 23, the last parameter of the setupChild() method passes in true, which indicates that the current View has been reclaimed before, so let's go back to the setupChild() method again:
/**
* 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);
}
}
As you can see, the last parameter of the setupChild() method is recycled, and then the variable is judged on line 32. Since recycled is now true, the attachViewToParent() method is executed, whereas the first Layout procedure is the addViewInLayout() method in the else statement executed. The biggest difference between the two methods is that if we need to add a new subview to the ViewGroup, we should call the addViewInLayout() method, and if we want to attach a previously detached View to the ViewGroup, we should call the attachViewToParent() method. So since the detachAllViewsFromParent() method was called earlier in the layoutChildren() method, so that all the subviews in ListView are in detach state, the attachViewToParent() method here is the right choice.
After such a detach and attach process, all the sub-views in List View can be displayed properly again, so the second Layout process ends.
Slide Load More Data
We've gone through two Layout processes, and although we can already see the content in ListView, we haven't touched on the most amazing part of ListView yet, because at present, ListView only loads and displays the data on the first screen. For example, our Adapter has 1000 data, but the first screen only shows 10, and the ListView only has 10 sub-views. So how does the remaining 990 work and display on the interface? Let's look at the source code for the ListView slider, because we show more data by sliding our fingers.
Since the mechanism of the sliding part is generic, that is, both ListView and GridView use the same mechanism, this part of the code must have been written in AbsListView. So listening for touch events is done in the onTouchEvent() method. Let's look at this method in AbsListView:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
final int action = ev.getAction();
View v;
int deltaY;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
&& (getAdapter().isEnabled(motionPosition))) {
// User clicked on an actual view (and was not stopping a
// fling). It might be a
// click or a scroll. Assume it is a click until proven
// otherwise
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
// If we couldn't find a view to click on, but the down
// event was touching
// the edge, we will bail out and try again. This allows
// the edge correcting
// code in ViewRoot to try to find a nearby view to
// select
return false;
}
if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRow(y);
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final int y = (int) ev.getY(pointerIndex);
deltaY = y - mMotionY;
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
// Check if we have moved far enough that it looks more like a
// scroll than a tap
startScrollIfNeeded(deltaY);
break;
case TOUCH_MODE_SCROLL:
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("AbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (y != mLastY) {
deltaY -= mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
// No need to do all this work if we're not going to move
// anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
if (atEdge && getChildCount() > 0) {
// Treat this like we're starting a new scroll from the
// current
// position. This will let the user start scrolling back
// into
// content immediately rather than needing to scroll
// back to the
// point where they hit the limit first.
int motionPosition = findMotionRow(y);
if (motionPosition >= 0) {
final View motionView = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = motionView.getTop();
}
mMotionY = y;
mMotionPosition = motionPosition;
invalidate();
}
mLastY = y;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mChild = child;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
mResurrectToPosition = motionPosition;
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
: mPendingCheckForLongPress);
}
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(child);
setPressed(true);
if (mSelector != null) {
Drawable d = mSelector.getCurrent();
if (d != null && d instanceof TransitionDrawable) {
((TransitionDrawable) d).resetTransition();
}
}
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_REST;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
}
return true;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
mTouchMode = TOUCH_MODE_REST;
break;
case TOUCH_MODE_SCROLL:
final int childCount = getChildCount();
if (childCount > 0) {
if (mFirstPosition == 0
&& getChildAt(0).getTop() >= mListPadding.top
&& mFirstPosition + childCount < mItemCount
&& getChildAt(childCount - 1).getBottom() <= getHeight()
- mListPadding.bottom) {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final int initialVelocity = (int) velocityTracker
.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinimumVelocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new FlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
break;
}
setPressed(false);
// Need to redraw since we probably aren't drawing the selector
// anymore
invalidate();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
if (PROFILE_SCROLLING) {
if (mScrollProfilingStarted) {
Debug.stopMethodTracing();
mScrollProfilingStarted = false;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);
View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
clearScrollingCache();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);
final int x = mMotionX;
final int y = mMotionY;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
return true;
}
There's a lot of code in this method because it handles a lot of logic and listens for all kinds of touch screen events. But what we are concerned about at present is only the event of finger sliding on the screen. The corresponding action is ACTION_MOVE, so we can just look at this part of the code.
As you can see, the ACTION_MOVE case has another switch statement nested in it, which is selected according to the current TouchMode. Here I can tell you directly that TouchMode is equal to TOUCH_MODE_SCROLL when your finger is sliding on the screen. As for why that involves several other methods, I will stop explaining them for space reasons. Friends who like to find out the root cause can go to the source code for their own reasons.
In this way, the code should go to the case on line 78. There is not much to be noticed in this case. The only important thing is that the trackMotionScroll() method called on line 92, which is equivalent to our finger moving slightly on the screen, will be called, and if it slides normally on the screen. If so, this method will be called many times. So let's go into this method and have a look. The code is as follows:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();
final Rect listPadding = mListPadding;
final int spaceAbove = listPadding.top - firstTop;
final int end = getHeight() - listPadding.bottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - getPaddingBottom() - getPaddingTop();
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
// Don't need to move views down if the top of the first position
// is already visible
return true;
}
if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
// Don't need to move views up if the bottom of the last position
// is already visible
return true;
}
final boolean down = incrementalDeltaY < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
final int top = listPadding.top - incrementalDeltaY;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
} else {
final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
invalidate();
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(getChildAt(childIndex));
}
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
awakenScrollBars();
return false;
}
This method receives two parameters. deltaY represents the distance from the position of the finger when it is pressed to the current position of the finger, and incrementalDeltaY represents the change in the position of the finger in the Y direction according to the last triggering event. In fact, we can judge whether the user is sliding up or down by the positive or negative value of incrementalDeltaY. As shown in line 34, if incrementalDeltaY is less than 0, it means sliding down, or sliding up.
The next step is to perform a boundary value detection process. As you can see, starting at line 43, when ListView slides downward, it goes into a for loop and gets the child View from top to bottom. In line 47, if the bottom value of the child View is less than the top value, it means that the child View has moved out of the screen, so the ScrapView () of RecycleBin is called. Method Add the View to the discarded cache and add the count counter to 1, which is used to record how many sub-Views have been removed from the screen. If the ListView slides upward, the process is basically the same, but it turns into getting the sub-View from bottom to top, and then deciding whether the top value of the sub-View is greater than the bottom value. If the top value is larger, the sub-View has moved out of the screen. Similarly, add it to the discarded cache and add the counter to 1.
Next, in line 76, a detach operation is performed based on the value of the current counter. Its function is to detach all the sub-views removed from the screen. In the concept of ListView, there is no need to save all the unseen views, because there are hundreds of data waiting to be displayed outside the screen. A good recovery strategy can ensure the height of the ListView. Performance and efficiency. Next, the offsetChildrenTopAndBottom() method is called on line 78 and incrementalDeltaY is passed in as a parameter. The function of this method is to make all the sub-views in ListView deviate accordingly according to the parameter values passed in, so that the content of ListView can scroll as the finger drags.
If the bottom of the last View in ListView has been moved to the screen, or the top of the first View in ListView has been moved to the screen, the fillGap() method will be called, so we can guess that the fillGap() method is used to load out-of-screen data. Go into this method and see, as follows:
/**
* Fills the gap left open by a touch-scroll. During a touch scroll,
* children that remain on screen are shifted and the other ones are
* discarded. The role of this method is to fill the gap thus created by
* performing a partial layout in the empty space.
*
* @param down
* true if the scroll is going down, false if it is going up
*/
abstract void fillGap(boolean down);
OK, fillGap() in AbsListView is an abstract method, so we can immediately think that its concrete implementation must have been completed in ListView. Back in ListView, the code for the fillGap() method is as follows:
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
getListPaddingTop();
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - getListPaddingBottom();
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
The down parameter is used to indicate whether the ListView is sliding down or up. You can see that if it is sliding down, the fillDown() method is called, and if it is sliding up, the fillUp() method is called. So we're all familiar with these two methods, both of which are filled through a loop, so we won't look at them, but filling ListView will be done by calling makeAndAddView() method and makeAndAddView() method, but this time the logic is different, so let's go back to this method and see:
/**
* 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;
}
Anyway, the first attempt here is to call RecycleBin's getActiveView() method to get the sub-layout, but it's certainly not possible, because we've already got data from mActiveViews in the second Layout process, and according to RecycleBin's mechanism, mActiveViews can't be reused, so the value returned here must be null.
Since the value returned by the getActiveView() method is null, it will still go to the obtainView() method in line 28. The code is as follows:
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
Here, in line 19, RecyleBin's getScrapView() method is called to try to get a View from the discarded cache, so does the discarded cache have a View? Of course, because we've just seen in the trackMotionScroll() method that once any child View is removed from the screen, it will be added to the obsolete cache, and from the logic of the obtainView() method, once new data needs to be displayed on the screen, it will try to get the View from the obsolete cache. So there's a producer-consumer model between them, and here's the magic of ListView. No matter how many pieces of data you have to display, there are only a few sub-views in ListView that go back and forth. Subviews that move out of the screen are quickly reused by the data that move into the screen, no matter how much data we load. There will be no OOM, and even no memory increase.
So another thing to note is that we get a scrapView here, and then we pass it as the second parameter in line 22 into the Adapter's getView() method. So what does the second parameter mean? Let's look again at a simple getView() method example:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
The second parameter is our most familiar convertView. No wonder we usually write getView() to determine whether convertView is equal to null. If it is equal to null, we call inflate() to load the layout. It does not mean that null can directly use convertView, because convertView is the View we used between us, but it is removed from the screen and entered into obsolescence. Abandoned in the cache, and now it's just taken out and used again. Then we just need to update the data in convertView to the data that should be displayed in the current location, so it looks like a completely new layout loaded. Have you fully understood the reason behind this?
After that, all the code is familiar with the process. We call setupChild() method to attach it back to ListView after we get the child View from the cache. Because the view in the cache was also detach from ListView before, this part of the code will not be repeated for analysis.
In order to facilitate your understanding, here I attach another illustration: