Of all the Android native controls, the most complex is probably the ListView, which is designed to handle situations where there are too many content elements to display on the phone screen. ListView displays content in the form of a list. Content that is outside the screen can be moved into the screen with a swipe of the finger.

One of the amazing things about ListView, which I’m sure you’ve all experienced, is that even if you load a very, very large amount of data into the ListView, like hundreds or even more, the ListView doesn’t get OOM or crash, and as you swipe through more data, The amount of memory used by the program doesn’t even grow. So how does ListView do this? At the beginning, I held the mentality of learning to spend a long time to read the ListView source code, the basic understanding of its working principle, in the sigh Google god can write such a subtle code at the same time I have some awe, because ListView code is relatively large, the complexity is also very high, it is difficult to use words to express clearly, So I dropped the idea of writing a blog. So now recall this matter I have intestines all regret, because not a few months I put the original comb clear source and forget. So now I’ve re-read the ListView source code again, and this time I’m going to write it as a blog post to share with you as well as my own notes.

First let’s look at the ListView inheritance structure, as shown in the following figure:

As you can see, the inheritance structure of the ListView is quite complex. It inherits directly from the AbsListView, and the AbsListView has two sub-implementation classes, one is ListView and the other is GridView. ListView and GridView have a lot in common in terms of how they work and how they are implemented. The AbsListView then inherits from the AdapterView, which inherits from the ViewGroup, and so on. Taking a look at the ListView inheritance structure will help us analyze the code more clearly later.

The role of the Adapter

Adapter is familiar to you, and we will use it when we use ListView. So have you thought about why you need an Adapter? It always feels like ListView is more complicated to use than other controls because of Adapter. So let’s take a look at what the Adapter really does.

In fact, controls are designed to interact with and display data, but ListView is more special, it is designed to display a lot of data, but ListView is only responsible for interaction and display, as to where the data comes from, ListView does not care. Therefore, the most basic ListView mode we can imagine is to have a ListView control and a data source.

However, if the ListView actually interacts with the data source directly, then the adaptation of the 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 every kind of data source, first of all, it will have poor scalability. There are several built-in ADAPTS and only several ADAPTS can be added dynamically. The second is that the ListView goes beyond what it’s supposed to do. It’s no longer just interactive and presentable, so the ListView becomes bloated.

Obviously, the Android development team would not allow this to happen, hence the emergence of Adapter. As the name suggests, Adapter acts as a bridge between the ListView and the data source. The ListView does not interact directly with the data source. Instead, the ListView uses Adapter as a bridge to access the real data source. Adapter interfaces are unified, so ListView doesn’t have to worry about any adaptation problems. Adapter is an interface, which can implement a variety of subclasses. Each subclass can use its own logic to perform specific functions and adapt to specific data sources. For example, ArrayAdapter can be used to adapt data sources of array and List. SimpleCursorAdapter can be used for cursor type data source adaptation, which is a very clever way to solve the problem of data source adaptation, and also has quite good scalability. A simple schematic diagram of the principle is shown below:

Of course, Adapter is not only for data source adaptation, there is another very important method that we need to rewrite in Adapter, and that is the getView() method, which will be covered in more detail in the following article.

RecycleBin mechanism

So before beginning to analyze ListView source code, there is a thing we need to understand in advance, is RecycleBin mechanism, this mechanism is ListView can achieve hundreds of thousands of data are not OOM one of the most important reasons. The RecycleBin code is not much, only about 300 lines, it is an internal class written in the AbsListView, so all subclasses that inherit from the AbsListView, that is, ListView and GridView, can use this mechanism. So let’s take a look at the RecycleBin code, 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. */ 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 the code

Here the RecycleBin code is not complete, I just put out the most important methods. So we will first of these methods for a simple interpretation, which will be a great help to the later analysis of the working principle of ListView.

  • FillActiveViews () This method takes two arguments, the first representing the number of views to store and the second representing the position value of the first visible element in the ListView. RecycleBin to use the mActiveViews array to store views, call this method will be based on the parameters passed to the ListView specified elements stored in the mActiveViews array.
  • GetActiveView () this method corresponds to fillActiveViews() and is used to retrieve data from the mActiveViews array. The method takes a position argument that represents the position of the element in the ListView, and the position value is automatically converted internally to the corresponding subscript value of the mActiveViews array. Note that mActiveViews are removed from mActiveViews once acquired. The next View retrieved from the same location will return null, which means that mActiveViews cannot be reused.
  • AddScrapView () is used to cache a deprecated View. This method takes a View argument and should be called when a View is determined to be deprecated (such as scrolling out of the screen). RecycleBin uses mScrapViews and mCurrentScrap lists to store discarded views.
  • The getScrapView() method is used to extract a View from the scrapheap cache. The views in the scrapheap cache have no order at all, so the algorithm in getScrapView() is very simple. Get the tail scrap View directly from mCurrentScrap and return it.
  • SetViewTypeCount () we all know that we can rewrite the Adapter getViewTypeCount() to indicate that there are several types of items in the ListView, The setViewTypeCount() method enables a separate RecycleBin cache mechanism for each type of item. In fact, the getViewTypeCount() method isn’t usually used that much, so you just need to know that it has one in RecycleBin.

After understanding the main RecycleBin method and their use, the following can start to analyze the working principle of ListView, here I will be in accordance with the previous analysis of source code to proceed, that is, follow the main line of the implementation process to gradually read and point to stop, If not, this article would be very, very long if all the ListView code were posted.

The first Layout

However, the ListView, however special, inherits from the View, so its execution process will follow the rules of the View. For those who are not familiar with this aspect, please refer to my Android View drawing process. Take you step by step into View(2).

The View execution process is divided into 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 to the interface. In listViews, onMeasure() is nothing special because it is a View that takes up the most space and usually the entire screen. OnDraw () also makes little sense in the ListView, because the ListView itself is not responsible for drawing, but rather draws from the child elements of the ListView. So most of the magic functions of ListView are actually carried out in the onLayout() method, so this article is also the main analysis of the content of this method.

If you look in the ListView source code, you will notice that the onLayout() method is not in the ListView. This is because the method is implemented in the ListView parent class, the AbsListView, as shown below:

/**
 * 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 the code

As you can see, the onLayout() method doesn’t do any complicated logic. It’s just a judgment that if the ListView changes size or position, the changed variable will change to true, forcing all child layouts to be redrawn. Other than that, there’s nothing hard to understand, but notice that the layoutChildren() method is called at line 16. From the name of the method, we can guess that this method is used for child layout, but when you go inside the method, you see that it’s an empty method with no lines of code. This is understandable, of course, because the layout of child elements should be the responsibility of the concrete implementation class, not the parent class. Enter the ListView layoutChildren() method 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;
        }
    }
}
Copy the code

This code is long, so let’s focus on it. First of all, there are no child views in the ListView. The data is still managed by the Adapter and is not displayed on the interface, so the getChildCount() method in line 19 must get a value of 0. And then on line 81, the execution logic will be judged based on the value of the dataChanged Boolean, which will only be true if the data source changes, otherwise it will be false, so it will be executed on line 90, Call fillActiveViews() of RecycleBin. The fillActiveViews() method is called to cache the child views of the ListView, but there are no child views in the ListView, so this line will not be used for the time being.

Then line 114 determines the layout mode based on the value of mLayoutMode. By default, it is LAYOUT_NORMAL, so it goes into the default statement on line 140. ChildCount is currently 0, and the default layout order is from top to bottom, so we enter the fillFromTop() method on line 145.

/**
 * 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 the code

As you can see from the comments for this method, its main job is to populate the ListView from top to bottom, starting with mFirstPosition. The fillDown() method has no logic. The fillDown() method calls the fillDown() method after checking the validity of the mFirstPosition value. 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; }Copy the code

As you can see, a while loop is used to repeat the logic, starting with nextTop being the pixel from the top of the first child to the top of the entire ListView, pos being the value of the mFirstPosition just passed in, End is the number of pixels from the bottom of the ListView minus the top, and mItemCount is the number of elements in the Adapter. So nextTop must be less than end at the beginning, and pos must be less than mItemCount. So every time we execute the while loop, pos is incremented by 1, and nextTop is incremented, so when nextTop is greater than or equal to end, which means the child is already out of the current screen, or pos is greater than or equal to mItemCount, When all the elements in the Adapter are iterated through, the while loop is broken.

So what does the while loop do? It’s worth noting that the makeAndAddView() method is called on line 18, entering it 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 the code

Here in line 19 we try to get a quick active View from RecycleBin, but unfortunately we don’t have any views cached in RecycleBin yet, so this must be null. After null, it will continue running until line 28 calls the obtainView() method to try to get a View again. This time the obtainView() method is guaranteed to return a View. The View is immediately passed into the setupChild() method. So how does obtainView() actually work inside? Let’s take a look at this method:

/** * 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 the code

The obtainView() method doesn’t have much code, but it contains very, very important logic, and literally the most important thing in the ListView is in this method. RecycleBin getScrapView() RecycleBin getScrapView() RecycleBin getScrapView(); The getScrapView() method returns a NULL. What should I do? It doesn’t matter, the code goes to line 33 and calls the mAdapter’s getView() method to get a View. So what is an mAdapter? Of course, the adapter associated with the current ListView. And what about the getView() method? The getView() method takes three arguments: Position, null, and this.

So what do we normally do when we write the ListView Adapter, the getView() method? Here’s 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 the code

The getView() method takes three arguments. The first argument, position, represents the position of the current child element. We can retrieve the data associated with the position. The second argument, convertView, which was passed in as null, means that there is no convertView to take advantage of, so we call the LayoutInflater’s inflate() method to load a layout. We then set some properties and values for the view, and finally return the view.

This View is also returned as the result of obtainView() and is eventually passed into the setupChild() method. This means that during the first layout process, all the child views are loaded by calling the LayoutInflater’s inflate() method, which is relatively time-consuming, but don’t worry, this won’t happen again, so we can continue:

/** * 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 the code

The setupChild() method has a lot of code, but it is very simple to look at the core code. The child View obtained by calling obtainView(), Here we call the addViewInLayout() method at line 40 to add it to the ListView. So according to the fillDown() method, the while loop tells the child View to fill up the ListView control and then jump out, which means that even if our Adapter has a thousand entries, the ListView will only load the first screen, The rest of the data isn’t currently visible on the screen anyway, so you don’t have to do any extra loading to ensure that the contents of the ListView are quickly displayed on the screen.

So that’s it. The first Layout procedure is over.

The second Layout

I can’t find the exact reason for this in the source code, but if you do your own experiments, you’ll find that even a simple View will go through at least two onMeasure() and two onLayout() processes before being displayed on the screen. In fact, this is only a small detail, which usually doesn’t affect us much, because no matter how many times onMeasure() or onLayout() is executed, it is the same logic, so we don’t need to pay too much attention to it. But in the ListView the situation is different, because that means that the layoutChildren() procedure is executed twice, which involves adding children to the ListView. If the same logic is executed twice, Then there will be a duplicate of the data in the ListView. So ListView in the process of layoutChildren() to do a second Layout logical processing, very clever to solve this problem, we will analyze the second Layout process.

In fact, the basic process of the second Layout and the first Layout is similar, so we still 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;
        }
    }
}
Copy the code

Again, at line 19, we call getChildCount() to get the number of child views, except instead of 0, we get the number of child Views that can be displayed on the screen of the ListView, Because we just added so many child views to the ListView during the first Layout process. RecycleBin fillActiveViews(); RecycleBin fillActiveViews(); RecycleBin fillActiveViews(); All child Views will then be cached in the RecycleBin mActiveViews array, which will be used later.

The next very, very important action is to call the detachAllViewsFromParent() method at line 113. This method removes all child views from the ListView so that the second Layout process does not produce duplicate data. That some friends may ask, so the View has been loaded and removed, and then have to reload again, this is not a serious impact on efficiency? Don’t worry, remember that we just called the fillActiveViews() method of RecycleBin to cache the child views, and we will use the cached views to load them directly instead of re-executing the inflate process, So there’s not a noticeable effect on efficiency.

So let’s see, in line 141, the judgment logic, since it’s no longer equal to zero, goes into the else statement. There are three more logical judgments in the else statement, and the first one doesn’t hold, because by default we haven’t selected any children, and the mSelectedPosition should be equal to -1. The second logical judgment is usually true because the value of mFirstPosition is equal to zero at first, as long as the data in the Adapter is greater than zero. So go to the fillSpecific() method and the code looks like this:

/** * Put a specific item at a specific location on the screen and then build * up and down from there. * * @param position The reference view to use as the starting point * @param top Pixel offset from the top of this view to the top of the * reference view. * * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific(int position, int top) { boolean tempIsSelected = position == mSelectedPosition; View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = position; View above; View below; final int dividerHeight = mDividerHeight; if (! mStackFromBottom) { above = fillUp(position - 1, temp.getTop() - dividerHeight); // This will correct for the top of the first view not touching the top of the list adjustViewsUpOrDown(); below = fillDown(position + 1, temp.getBottom() + dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooHigh(childCount); } } else { below = fillDown(position + 1, temp.getBottom() + dividerHeight); // This will correct for the bottom of the last view not touching the bottom of the list adjustViewsUpOrDown(); above = fillUp(position - 1, temp.getTop() - dividerHeight); int childCount = getChildCount(); if (childCount > 0) { correctTooLow(childCount); } } if (tempIsSelected) { return temp; } else if (above ! = null) { return above; } else { return below; }}Copy the code

FillSpecific () is a new method, but it does exactly the same thing as fillUp() and fillDown(). The main difference is that fillSpecific() will first load the child View at the specified location onto the screen. And then we load that child View and all the other child views that go up and down. So since the position we pass in here is the position of the first child View, the fillSpecific() method is basically the same as the fillDown() method, and we won’t pay too much attention to the details here, Instead, focus on the makeAndAddView() method. Back to the makeAndAddView() method again, the code looks like this:

/** * 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 the code

The RecycleBin RecycleBin method is used to cache the child View. The RecycleBin method is used to cache the child View in fillActiveViews(). In this case, instead of going to the obtainView() method on line 28, you go directly to the setupChild() method, which saves a lot of time, because if you had to go to infalte in the obtainView() method again, The ListView’s initial loading efficiency is greatly reduced.

Notice in line 23 that the last argument to the setupChild() method is passed true. This argument indicates that the current View was previously recycled, so we are back in 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 the code

As you can see, the last argument to the setupChild() method is recycled, and then in line 32 it evaluates this variable. Since recycled is now true, the attachViewToParent() method is executed, The first Layout procedure is the addViewInLayout() method in the else statement that executes. The big 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, If you want to attach a previously detach View back to the ViewGroup, you should call attachViewToParent(). Since we called the detachAllViewsFromParent() method in layoutChildren(), all the child views in the ListView are in detach state, So here the attachViewToParent() method is the correct choice.

After detach and attach, all sub-views in the ListView can be displayed normally, so the second Layout process is over.

Space is limited, so continue in the next article.