The working principle of Android ListView is completely analyzed, and you can thoroughly understand it from the perspective of source code (1)

The working principle of Android ListView is completely analyzed, and you can thoroughly understand it from the perspective of source code (1)

Among all the commonly used native controls in Android, the most complicated to use is ListView, which is specifically used to deal with the situation where there are many content elements and the mobile screen cannot display all the content. ListView can display content in the form of a list, and the content beyond the screen can be moved to the screen by sliding the finger.

In addition, ListView also has a very magical function. I believe everyone should have experienced it. Even if a very, very large amount of data is loaded in the ListView, such as hundreds or even more, the ListView will not OOM or crash, and As we swipe our finger to browse more data, the memory occupied by the program will not increase. So how does ListView achieve such a magical function? At the beginning, I spent a long time reading the source code of ListView with the mentality of learning, and I basically understood its working principle. While lamenting that Google God could write such a delicate code, I was also in awe, because of the code of ListView. The volume is relatively large and the complexity is also high, it is difficult to express clearly in words, so I gave up the idea of writing it as a blog. So now I look back on this matter and I already regret it, because within a few months I have forgotten the source code that was clearly sorted out. So now I am reconciled to read the source code of ListView again, so this time I must write it as a blog, and share it with everyone as my own notes.

1. let's take a look at the inheritance structure of ListView, as shown in the following figure:

As you can see, the inheritance structure of ListView is quite complicated, it is directly inherited from AbsListView, and AbsListView has two sub-implementation classes, one is ListView, the other is GridView, so we can guess from this point, ListView and GridView has a lot in common in its working principle and implementation. Then AbsListView inherits from AdapterView, and AdapterView inherits from ViewGroup, which is what we know later. Let's first understand the inheritance structure of ListView, which will help us analyze the code more clearly later.

The role of Adapter

Adapter is not unfamiliar to everyone, we will definitely use it when we use ListView. So having said that, have you ever thought about why you need Adapter? I always feel that because of the Adapter, the use of ListView has become much more complicated than other controls. So here we will first learn what kind of 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 display a lot of data, but ListView only undertakes interaction and display work, as for where the data comes from, ListView does not care of. Therefore, the most basic working mode of ListView we can imagine is to have a ListView control and a data source.

But if you really let the ListView interact with the data source directly, then the adaptation work that the ListView has to do is very complicated. Because the concept of data source is too vague, we only know that it contains a lot of data. As for what type of data source is, there is no strict definition. It may be an array, a collection, or even a database. The cursor queried in the table. Therefore, if the ListView really performs adaptation operations for each data source, first, the scalability will be relatively poor. There are only a few types of adaptations built-in, and they cannot be added dynamically. The second is beyond the scope of work it should be responsible for, it is no longer just to undertake interaction and display work, so the ListView will become more bloated.

So obviously the Android development team will not allow this to happen, so there is a mechanism like Adapter. As the name suggests, Adapter means an adapter. It acts as a bridge between ListView and data source. ListView does not directly deal with the data source, but will use the Adapter bridge to access the real data source. The difference is that the interface of the Adapter is unified, so ListView no longer has to worry about any adaptation issues. And Adapter is an interface (interface), it can implement a variety of subclasses, each subclass can use its own logic to complete specific functions, as well as the adaptation operation with a specific data source, for example ArrayAdapter can be used for array and List type data source adaptation, SimpleCursorAdapter can be used for cursor type data source adaptation, so that the problem of difficult data source adaptation is solved very cleverly, and it also has a pretty good extension Sex. The simple schematic diagram is as follows:

Of course, the role of the Adapter is not only the 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 analyzing the source code of ListView, there is one more thing we need to know in advance, that is, the RecycleBin mechanism. This mechanism is also the most important reason why ListView can achieve hundreds of thousands of data without OOM. In fact, the code of RecycleBin is not much, only about 300 lines. It is an internal class written in AbsListView, so all subclasses inherited from AbsListView, namely ListView and GridView, can use this mechanism. Let's take a look at the main code in RecycleBin, as shown below:

/** * The RecycleBin facilitates reuse of views across layouts. The RecycleBin * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are * those views which were onscreen at the start of a layout. By * construction, they are displaying current information. At the end of * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews * are old views that could potentially be used by the adapter to avoid * allocating views unnecessarily. * * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) * @see android.widget.AbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * Views that were on screen at the start of layout. This array is * populated at the start of layout, and at the end of layout all view * in mActiveViews are moved to mScrapViews. Views in mActiveViews * represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; /** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount * The minimum number of views mActiveViews should hold * @param firstActivePosition * The position of the first view that will be stored in * mActiveViews */ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length <childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; final View[] activeViews = mActiveViews; for (int i = 0; i <childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); //Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { //Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in //active views. //However, we will NOT place them into scrap views. activeViews[i] = child; } } } /** * Get the view corresponding to the specified position. The view will * be removed from mActiveViews if it is found. * * @param position * The position to look up in mActiveViews * @return The view if it is found, null otherwise */ View getActiveView(int position) { int index = position-mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >= 0 && index <activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; } /** * Put a view into the ScapViews list. These views are unordered. * * @param scrap * The view to add */ void addScrapView(View scrap) { AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } //Don't put header or footer views or views that should be ignored //into the scrap heap int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(scrap, false); } return; } if (mViewTypeCount == 1) { dispatchFinishTemporaryDetach(scrap); mCurrentScrap.add(scrap); } else { dispatchFinishTemporaryDetach(scrap); mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } /** * @return A view from the ScrapViews collection. These are unordered. */ View getScrapView(int position) { ArrayList<View> scrapViews; if (mViewTypeCount == 1) { scrapViews = mCurrentScrap; int size = scrapViews.size(); if (size> 0) { return scrapViews.remove(size-1); } else { return null; } } else { int whichScrap = mAdapter.getItemViewType(position); if (whichScrap >= 0 && whichScrap <mScrapViews.length) { scrapViews = mScrapViews[whichScrap]; int size = scrapViews.size(); if (size> 0) { return scrapViews.remove(size-1); } } } return null; } public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount <1) { throw new IllegalArgumentException("Can't have a viewTypeCount <1"); } //noinspection unchecked ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i <viewTypeCount; i++) { scrapViews[i] = new ArrayList<View>(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } } Copy code

The RecycleBin code here is not complete, I just mentioned the most important methods. So let's first make a simple interpretation of these methods, which will be of great help in analyzing the working principle of ListView later.

  • The fillActiveViews() method receives two parameters. The first parameter indicates the number of views to be stored, and the second parameter indicates the position value of the first visible element in the ListView. RecycleBin uses the mActiveViews array to store Views. After calling this method, the specified elements in the ListView will be stored in the mActiveViews array according to the passed parameters.
  • The getActiveView() method corresponds to fillActiveViews() and is used to get data from the mActiveViews array. This method receives a position parameter, which indicates the position of the element in the ListView. The method automatically converts the position value into the subscript value corresponding to the mActiveViews array. It should be noted that the Views stored in mActiveViews will be removed from mActiveViews once they are acquired, and the View at the same location will return null next time, which means that mActiveViews cannot be reused.
  • addScrapView() is used to cache a discarded View. This method receives a View parameter. When a View is determined to be discarded (such as scrolling out of the screen), this method should be called to cache the View. Two Lists, mScrapViews and mCurrentScrap, are used in RecycleBin to store discarded Views.
  • getScrapView is used to fetch a View from the discarded cache. The Views in these discarded caches are in no order. Therefore, the algorithm in the getScrapView() method is also very simple, which is to obtain a scrap view at the tail directly from mCurrentScrap and return it.
  • setViewTypeCount() We all know that in the Adapter, a getViewTypeCount() can be overridden to indicate that there are several types of data items in the ListView, and the function of the setViewTypeCount() method is to enable a separate RecycleBin caching mechanism for each type of data item. In fact, the getViewTypeCount() method is not usually used a lot, so we only need to know that there is such a function in RecycleBin.

After understanding the main methods in RecycleBin and their usefulness, you can start to analyze the working principle of ListView. Here I will continue to analyze the source code in the previous way, that is, follow the main line execution process to gradually read and click. Stop, otherwise, if you post all the code of ListView, then this article will be very long.

Layout for the first time

In any case, ListView is ultimately inherited from View, no matter how special, so its execution flow will still be executed according to the rules of View. For those who are not familiar with this aspect, please refer to the Android view drawing process I wrote before for a  complete analysis. , Take you step by step in-depth understanding of View (2)  .

The execution flow of View is nothing more than three steps, onMeasure() is used to measure the size of the View, onLayout() is used to determine the layout of the View, and onDraw() is used to draw the View onto the interface. In ListView, onMeasure() has nothing special, because it is a View after all, which takes up the most space and is 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 of ListView. So most of the magical functions of ListView are actually carried out in the onLayout() method, so this article is also the main analysis of the content in this method.

If you look for the ListView source code, you will find that there is no onLayout() method in ListView. This is because this method is implemented in AbsListView, the parent class 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; } Copy code

As you can see, the onLayout() method does not do any complicated logical operations, it is mainly a judgment. If the size or position of the ListView changes, the changed variable will become true, and all sub-layouts will be required at this time Both are forced to redraw. Other than that, there is nothing difficult to understand, but we noticed that the layoutChildren() method is called on line 16. From the method name, we can guess that this method is used for the layout of child elements. But when you enter this method, you will find that this is an empty method without a line of code. This is of course understandable, because the layout of the child elements should be completed by the specific implementation class, not by the parent class. 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 (ie 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; } } } Copy code

This code is relatively long, let's focus on it. The first thing to be sure is that there are currently no child views in the ListView, and the data is still managed by the Adapter and not displayed on the interface, so the value obtained by the getChildCount() method on line 19 must be 0. Then in line 81, the execution logic will be judged based on the boolean value of dataChanged. dataChanged will only become true when the data source is changed. Otherwise, it will be false. Therefore, it will enter the execution of line 90 here. Logic, call the fillActiveViews() method of RecycleBin. It stands to reason that the fillActiveViews() method is called to cache the child views of the ListView, but currently there are no child views in the ListView, so this line does not have any effect for the time being.

Next, on line 114, the layout mode will be determined according to the value of mLayoutMode. By default, it is the normal mode LAYOUT_NORMAL, so it will enter the default statement on line 140. And the next two if judgments will be performed immediately, childCount is currently equal to 0, and the default layout order is from top to bottom, so it will enter the fillFromTop() method on line 145. Let s take a look:

/** * 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); } Copy code

As can be seen from the comments of this method, the main task it is responsible for is to fill the ListView from top to bottom starting from mFirstPosition. There is no logic in this method itself. It just judges the validity of the mFirstPosition value and then calls the fillDown() method. Then we have reason to guess that the operation of filling the ListView is done in the fillDown() method. Enter the fillDown() method, the code is as follows:

/** * 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; } Copy code

As you can see, a while loop is used to perform the repetition logic. At the beginning, 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 mFirstPosition just passed in, and end is the ListView The pixel value obtained by subtracting the top from the bottom, mItemCount is the number of elements in the Adapter. Therefore, at the beginning, nextTop must be less than the end value, and pos is also less than the mItemCount value. Then every time the while loop is executed, the value of pos will increase by 1, and the value of nextTop will also increase. When nextTop is greater than or equal to end, that is, the child element has exceeded the current screen, or when pos is greater than or equal to mItemCount, that is, all Adapters After the elements have been traversed, the while loop will jump out.

So what is done in the while loop? It is worth noting that the makeAndAddView() method called on line 18 is entered into this method. 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; } Copy code

Here, in line 19, I try to quickly get an active view from RecycleBin, but unfortunately there is no view cached in RecycleBin, so the value obtained here must be null. Then after obtaining the null, it will continue to run down, and at line 28, the obtainView() method will be called to try to obtain a View again. This time the obtainView() method can guarantee to return a View, so the following will get it immediately The received View is passed into the setupChild() method. So how does obtainView() work internally? Let's take a look at this method first:

/** * 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; } Copy code

There is not much code in the obtainView() method, but it contains very, very important logic. It is not an exaggeration to say that the most important content in the entire ListView may be in this method. So we still follow the execution process, in the 19th line of code, the getScrapView() method of RecycleBin is called to try to obtain a View in the discarded cache. For the same reason, it is definitely not available here. The getScrapView() method will Returns a null. What should I do at this time? It doesn't matter, the code will execute to line 33 and call the getView() method of mAdapter to get a View. So what is mAdapter? Of course it is the adapter associated with the current ListView. And what is the getView() method? Needless to say, this is one of the most frequently rewritten methods when we usually use ListView. Here, three parameters are passed in to the getView() method, namely position, null and this.

So how do we usually write the getView() method when we write the Adapter of ListView? Here I give 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; } Copy code

The getView() method accepts three parameters. The first parameter position represents the position of the current child element, and we can get the data related to it through the specific position. 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, set some attributes and values of this view, and finally return the view.

Then this View will also be returned as the result of obtainView() and finally passed into the setupChild() method. In fact, in the first layout process, all child views are loaded by calling the inflate() method of LayoutInflater, which will be relatively time-consuming, but don t worry, there will be no such situation in the future. , Then we continue to look down:

/** * 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); } } Copy code

Although there are many codes in the setupChild() method, it is very simple if we only look at the core code. The child element View obtained by calling the obtainView() method just now, here is called the addViewInLayout() method in line 40 to add it Into the ListView. Then according to the while loop in the fillDown() method, the child element View will fill the entire ListView control and then jump out. That is to say, even if there are a thousand pieces of data in our Adapter, the ListView will only load the data of the first screen. , The remaining data is currently not visible on the screen anyway, so no extra loading work will be done, so that you can ensure that the content in the ListView can be quickly displayed on the screen.

So far, the first Layout process is over.

2.Layout

Although I did not find out the specific reason in the source code, if you do the experiment yourself, you will find that even a simple View will go through at least twice onMeasure() and twice before it is displayed on the interface. The process of onLayout(). In fact, this is only a small detail, and it doesn't affect us much, because whether it is onMeasure() or onLayout() several times, the same logic is executed anyway, and we don't need to care too much. But the situation in ListView is different, because it means that the layoutChildren() process will be executed twice, and this process involves adding child elements to the ListView. If the same logic is executed twice, then the ListView will There will be a duplicate data. Therefore, ListView does the second Layout logic processing in the layoutChildren() process, which solves this problem very cleverly. Let's analyze the second Layout process.

In fact, the basic process of the second Layout and the first Layout is similar, so let's 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 (ie 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; } } } Copy code

Also in line 19, call the getChildCount() method to get the number of child views, but now the value obtained will not be 0 anymore, but the number of child views that can be displayed on one screen in the ListView, because we have just been in the first So many child Views are added to ListView in a Layout process. The fillActiveViews() method of RecycleBin is called below on line 90. The effect is different this time, because there are already child views in the ListView, so all child views will be cached in the mActiveViews array of RecycleBin. Will use them.

Next will be a very, very important operation. The detachAllViewsFromParent() method is called on line 113. This method will clear all the child Views in all ListViews, so as to ensure that the second Layout process will not produce a duplicate data. Then some friends may ask, this way, the already loaded View is cleared, and it will be reloaded again later. Doesn't this seriously affect efficiency? Don t worry, remember that we just called the fillActiveViews() method of RecycleBin to cache the child Views. Later, we will directly use these cached Views to load instead of re-executing the inflate process, so the efficiency is There will be no obvious impact.

Then let's look at the judgment logic on line 141, since it is no longer equal to 0, it will enter the else statement. There are three more logical judgments in the else statement. The first logical judgment is not true, because by default we have not selected any child elements, mSelectedPosition should be equal to -1. The second logical judgment is usually true, because the value of mFirstPosition is equal to 0 at the beginning, and the condition is true as long as the data in the adapter is greater than 0. Then enter the fillSpecific() method, 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; } } Copy code

fillSpecific() This is a new method, but in fact it has the same function as the fillUp() and fillDown() methods. The main difference is that the fillSpecific() method will first load the child View at the specified position on the screen. Then load the other sub-Views up and down the sub-View. Then since the position we passed in here is the position of the first child View, the function of the fillSpecific() method is basically the same as the fillDown() method. Here we don t pay much attention to its details, and 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; } Copy code

I still try to get the Active View from RecycleBin on line 19, but this time I can get it because we called the fillActiveViews() method of RecycleBin to cache the child views. So in this case, you will not enter the obtainView() method on line 28, but will directly enter the setupChild() method, which also saves a lot of time, because if you have to go to the infalte in the obtainView() method With layout, the initial loading efficiency of ListView is greatly reduced.

Note that in line 23, the last parameter of the setupChild() method is passed in true. This parameter indicates that the current View has been recycled before, so we return 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); } } Copy code

As you can see, the last parameter of the setupChild() method is recycled, and then this variable will be judged in line 32. Since recycled is now true, the attachViewToParent() method will be executed, and the first Layout process is executed The addViewInLayout() method in the else statement. The biggest difference between these two methods is that if we need to add a new child View to the ViewGroup, we should call the addViewInLayout() method, and if we want to reattach a previously detached View to the ViewGroup, we should call attachViewToParent () Method. Then, because the detachAllViewsFromParent() method was called in the layoutChildren() method, so all the child views in the ListView are in the detach state, so the attachViewToParent() method is the correct choice.

After such a detach and attach process, all the child views in the ListView can be displayed normally again, then the second Layout process ends.

Space is limited, so I will continue with the next article.