The introduction

As mentioned in the previous View rendering mechanism of Android, the measurement and layout of controls will be realized by onMeasure() and onLayout() methods. So here we use these two functions as the entrance to study the whole layout process of RecyclerView.

basis

RecyclerView is more flexible than the previous ListView. The division of labor of each class is more clear, which well embodies the principle of single responsibility that we often call. Let’s take a look at the classes used here

  • LayoutManager: RecyclerView LayoutManager, mainly responsible for the measurement and layout of RecyclerView sub-view.
  • Recyclerview. Recycler: The core class for caching. RecyclerView powerful cache ability is based on this class to achieve. Is the core utility class for caching.
  • Adapter: The base class of the Adapter. Responsible for binding data in ViewHolder and controls in RecyclerView.
  • ViewHolder: View and metadata classes. It holds the data information to display, including location, View, ViewType, and so on.

The source code

Whether View or ViewGroup subclass, are through onMeasure() to achieve measurement work, then we for RecyclerView source code analysis onMeasure as our entry point

Its measurement

    //RecyclerView.java
	protected void onMeasure(int widthSpec, int heightSpec) {
        / / dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3 will perform, but according to the specific situation to distinguish in onMeasure onLayout.
        if (mLayout == null) {// If LayoutManager is empty, the default measurement strategy is used
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            // LayoutManager is enabled for automatic measurement
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            // Step 1 Call the onMeasure method of LayoutManager to perform the measurement
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            // If width and height are already exact values, then the measurement is no longer required
            if (skipMeasure || mAdapter == null) {
                return;
            }
            // If the width or height of the measurement process is not accurate, then the layout needs to be based on child to determine its width and height.
            // The current layout state is start
            if (mState.mLayoutStep == State.STEP_START) {
                // The first part of the layout does some initialization
                dispatchLayoutStep1();
            }
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            // Execute the second layout step. First check the size and layout of the child View
            dispatchLayoutStep2();
            // At the end of the layout process, calculate and set the length and width of RecyclerView according to the boundary information in Children
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            // Check if this measurement is needed again. If RecyclerView still has an imprecise width and height, or at least one Child here has an imprecise width and height, we need to measure again.
            // If the parent size attributes depend on each other, change the parameters and start again
            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                // now we can get the width and height from the children.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); }}else {
            // LayoutManager is enabled, automatic measurement is not enabled. The three layoutManagers of a general system are all automatic measurements,
            // setAutoMeasureEnabled can be used to disable automatic measurement for our custom LayoutManager
            //RecyclerView has set a fixed Size, directly use a fixed value can be
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            // If the data changes during the measurement, the data needs to be processed first.// After processing the newly updated data, perform the custom measurement operation.
            if(mAdapter ! =null) {
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            eatRequestLayout();
            // If there is no fixed width and height set, it needs to be measured
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            resumeRequestLayout(false);
            mState.mInPreLayout = false; }}Copy the code

In this method, different treatments are carried out according to different situations.

  1. Use the default measurement method without setting LayoutManager.
  2. When Layoutmanager is set and automatic measurement is enabled.
  3. LayoutManager is set but automatic measurement is not enabled.

Let’s start with the simplest one.

First: LayoutManager is not set.

Because all the measurement and layout work of RecyclerView is handled by LayoutManager, the default measurement scheme can only be used if it is not set.

Third: LayoutManager, and turn off the automatic measurement function.

You don’t need to consider the size and layout of the child View when you turn off the measurement. Directly follow the normal process for measurement. If a fixed width and height are set directly, use the fixed value. If there is no fixed width and height, then in accordance with the normal control, according to the requirements of the parent and its own properties for measurement.

The second one has LayoutManager, which enables automatic measurement.

This case is the most complex and requires resizing itself based on the layout of the child View. You need to know the size and layout of the child View. So RecyclerView will advance the layout process here.

So let’s simplify the code and see

//RecyclerView.java
if (mLayout.mAutoMeasure) {
    		// Call LayoutManager's onMeasure method to perform the measurement
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            // If width and height are already exact values, then the measurement is no longer required
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                // The first part of the layout does some initializationdispatchLayoutStep1(); }...// Enable automatic measurement, need to confirm the size and layout of the child ViewdispatchLayoutStep2(); .// Determine the size of the child View
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            if (mLayout.shouldMeasureTwice()) {
                ...
                // If the parent size attributes depend on each other, change the parameters and start againdispatchLayoutStep2(); mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); }}Copy the code

For the measurement and drawing of RecyclerView, dispatchLayoutStep1, dispatchLayoutStep2, dispatchLayoutStep3 are needed to execute the three steps, step1 is to carry out the pre-layout, It is mainly related to the information required by the animation required when the data is updated. Step2 is the step of the measurement layout of the sub-view, and step3 is mainly used to actually execute the animation. And mLayoutStep records which step is currently executed. If the fixed width and height are not set when automatic measurement is enabled, setp1 and step2 will be executed. After step2 execution can call setMeasuredDimensionFromChildren method, according to the results of measurement layout subclass to set their own size.

We will not analyze the specific functions of step1, step2 and step3. Post the onLayout code directly to see how all three steps work.

    //RecyclerView.java
	@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        dispatchLayout();
    }
    
    void dispatchLayout(a) {
        if (mAdapter == null) {// No adapter is set
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {// LayoutManager is not set
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        // In the onMeasure phase, if the width and height are fixed, then mLayoutStep == state. STEP_START and dispatchLayoutStep1 and dispatchLayoutStep2 are not called
        // So this will be called
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if(mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth()|| mLayout.getHeight() ! = getHeight()) {// In the onMeasure phase, if dispatchLayoutStep1 is executed but dispatchLayoutStep2 is not executed, then dispatchLayoutStep2 is executed
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        // Finally call dispatchLayoutStep3
        dispatchLayoutStep3();
    }
Copy the code

It can be seen that in the onLayout stage, which of the three steps in the onMeasure stage will be executed, and then the remaining steps will be executed in onLayout.

OK, now the whole process is through, in the three steps, step2 is the implementation of the sub-view measurement layout step, is also the most important part, so we will focus on this function.

    //RecyclerView.java
	private void dispatchLayoutStep2(a) {
        // Disable layout requestseatRequestLayout(); . mState.mInPreLayout =false;
        // Call the layoutChildren method of LayoutManager for layoutmLayout.onLayoutChildren(mRecycler, mState); . resumeRequestLayout(false);
    }
Copy the code

The onLayoutChildren method of LayoutManager is called here, handing over the measurement and layout of the child views to LayoutManager. We also had to override this method to describe our layout error when we were customizing LayoutManager. Here we analyze the most commonly used LinearLayoutManager(LLM). We’re only dealing with vertical layouts here.

    //LinearLayoutManager.java
	public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        // item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
Copy the code

At the beginning of the method, we are thrown a piece of documentation that tells us the layout strategy in the LinearLayoutManager. A brief translation:

  1. Through child controls and other variable information. Locate an anchor point and anchor point item.
  2. Starting at the location of the anchor point, work up and fill the layout subview until the area is filled
  3. Starting at the location of the anchor point, fill the layout subview downward until the area is filled
  4. Scroll to meet requirements such as stack from the bottom

The key word here is AnchorInfo, but LLMS are not laid out from top to bottom. Rather, it’s likely to start somewhere in the middle of the layout, fill in one direction, fill in the visible area, and fill in the other direction. Which direction to fill first depends on specific variables.

Anchor point selection

The AnchorInfo class needs to be able to effectively describe a specific location, starting with several important member variables inside the class.

    //LinearLayoutManager.java
	// A simple data class to hold anchor information
    class AnchorInfo {
        // The anchor refers to the View's position in the entire data
        int mPosition;
        // Fill in the initial coordinates of the subview with the anchor point coordinates. When positon=0, if only half of the View is visible, then this data may be negative
        int mCoordinate;
        // Whether to start the layout from the bottom
        boolean mLayoutFromEnd;
        // Whether it is valid
        boolean mValid;
Copy the code

As you can see, AnchorInfo allows you to pinpoint your current location. So how is the location of this anchor point determined in the LLM?

We look for the answer in the source code.

    //LinearLayoutManager.java
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {...// Verify that LayoutState exists
        ensureLayoutState();
        // Disallow collection
        mLayoutState.mRecycle = false;
        // Calculate whether to draw upside down. Draw from bottom to top or from top to bottom (in the LLM constructor, you can set the reverse drawing)
        resolveShouldLayoutReverse();
        // If the current anchor information is illegal, the slide location is unavailable or there is a stored SaveState that needs to be recovered
        if(! mAnchorInfo.mValid || mPendingScrollPosition ! = NO_POSITION || mPendingSavedState ! =null) {
            // Reset the anchor information
            mAnchorInfo.reset();
            // Whether to start the layout from end. Since mShouldReverseLayout and mStackFromEnd are both false by default, we can consider using the default analysis here, that is, mLayoutFromEnd is also false
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // Calculate the position and coordinates of the anchor points
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            // Set the anchor point to work
            mAnchorInfo.mValid = true;
        }
Copy the code

When need to make sure that the anchor will be initialized to anchor, and then through updateAnchorInfoForLayout method to determine the anchor point of information.

    //LinearLayoutManager.java
	private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
        // Update anchor information from pending data this method is not normally called
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            return;
        }
        //** The focus method determines the anchor information from the child View (try to determine the anchor information from the child View that has the focus or the first position of the list View or the last position of the View)
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            return;
        }
        // Set the top/bottom of RecyclerView as the RecyclerView anchor (mPosition=0 by default).
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
Copy the code

There are three main schemes for determining anchor points:

  1. Get anchor information from hanging data. It’s usually not executed.
  2. Determine anchor information from the child View. For example, the notifyDataSetChanged method, which originally has a View on the screen, will be retrieved this way
  3. If neither method is certain, use the View at position 0 as the anchor reference position.

When does the last one happen? There’s just no child View for us to reference. For example, when loading data for the first time, RecyclerView is blank. There certainly isn’t any child View we can reference at this point.

So when we have a child View, we use the updateAnchorFromChildren method to determine the anchor position.

    //LinearLayoutManager.java
	// Determine the anchor from an existing subview. In most cases, it is a valid child View at the beginning or end (usually the View that is not removed and is displayed in front of us).
    private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
        // No data, return false
        if (getChildCount() == 0) {
            return false;
        }
        final View focused = getFocusedChild();
        // Select the subview that gets the focus first as the anchor
        if(focused ! =null && anchorInfo.isViewValidAsAnchor(focused, state)) {
            // Hold the position of the child view that gets the focus
            anchorInfo.assignFromViewAndKeepVisibleRect(focused);
            return true;
        }
        if(mLastStackFromEnd ! = mStackFromEnd) {return false;
        }
        // Get the child View information from the bottom or top, depending on the anchor Settings
        View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state);
        if(referenceChild ! =null) { anchorInfo.assignFromView(referenceChild); .return true;
        }
        return false;
    }
Copy the code

Through the sub-view to determine the anchor point coordinates are also three cases of processing

  1. No data, return fetch failure
  2. If a child View holds a focus, use the child View that holds the focus as the anchor reference point
  3. No child View holds the focus, and the topmost (or bottom) child View is usually chosen as the anchor reference point

In general, the third method is used to determine the anchor point, so we will focus on this method here. According to our default variable information, there will be obtained through findReferenceChildClosestToStart visible region of the first child View as anchor point View of reference. The assignFromView method is then called to determine several attribute values of the anchor point.

		//LinearLayoutManager.java
		public void assignFromView(View child) {
            if (mLayoutFromEnd) {
                // If the layout is from the bottom, the bottom position of the fetch child is set to the anchor point
                mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
            } else {
                // If the layout starts from the top, set the position of the top of the fetch child to the anchor coordinate (consider ItemDecorator here)
                mCoordinate = mOrientationHelper.getDecoratedStart(child);
            }
            //mPosition assigns the position of the reference View
            mPosition = getPosition(child);
        }
Copy the code

MPostion is pretty straightforward, it’s the location of the child View, so what’s mCoordinate? Let’s see how we handle getDecoratedStart.

        //LinearLayoutManager.java
        // Create mOrientationHelper. We analyze it in a vertical layout
        if (mOrientationHelper == null) {
            mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
        }
        //OrientationHelper.java
    public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            @Override
            public int getDecoratedStart(View view) {
                final RecyclerView.LayoutParams params =  (RecyclerView.LayoutParams)view.getLayoutParams();
                //
                return mLayoutManager.getDecoratedTop(view) - params.topMargin;
            }

Copy the code

Is that a little confusing? Let’s explain it in the last crude picture

You can see that when confirming anchor information with a child control, the position of the child View visible on the screen is generally selected as the anchor. Here, the first visible View on the screen, namely the sub-view of Positon =1, will be selected as the reference point, and mCoordinate is assigned to the top location of Decor on sub-view no. 1.

Layout filling

Back to the main line onLayoutChildren function. Once our anchor information is confirmed, all that remains is to populate the layout from this location.

        if (mAnchorInfo.mLayoutFromEnd) {// Start the layout from end
            // To draw backwards, draw first from the anchor point up, and then from the anchor point down
            // Set the drawing direction information to up from the anchor point
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // Set the drawing direction information to from the anchor point down
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; }}else {// Start the layout from the starting position
            // Update layoutState to set the layout orientation downward
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            // Start filling in the layout
            fill(recycler, mLayoutState, state, false);
            // End offset
            endOffset = mLayoutState.mOffset;
            // Position of the last view after drawing
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // Update layoutState to set the layout orientation up
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            // Fill the layout again
            fill(recycler, mLayoutState, state, false);
            // The offset of the starting position
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; }}Copy the code

As you can see, depending on the drawing direction, there is a different process, but the filling direction is opposite, and the specific steps are similar. The View is filled from the anchor point in one direction, and then filled in the other direction. The child View is filled with the fill() method.

Because the drawing direction is handled by default, here we look at the code that analyzes the else, and the first fill is downward.

    // In the LinearLayoutManager, the job of filling the screen with sub-views is to call Fill () in both cases of interface redrawing and sliding
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        // The number of pixels in the available area
        final int start = layoutState.mAvailable;
        if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) {if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // Recycle the View that slides off the screen
            recycleByLayoutState(recycler, layoutState);
        }
        // Remaining drawing space = available area + extended space.
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        // Loop through the layout until there is no more space or data left
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // Initialize layoutChunkResult
            layoutChunkResult.resetInternal();
            //** The key method adds a child and saves the drawing information to layoutChunkResult
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {// If the layout is finished (no view left), exit the loop
                break;
            }
            // Update the offset of layoutState based on the height consumed by the added child. MLayoutDirection is either +1 or -1, and you can multiply to see if you're laying out from the bottom or from the top to the bottom
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(! layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList ! =null| |! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed;// Consume free spaceremainingSpace -= layoutChunkResult.mConsumed; }... }// Returns the area filled with this layout
        return start - layoutState.mAvailable;
    }
Copy the code

In the fill method, it determines whether there is any area left for the child View to fill. If there is no remaining area or no child View, then return. Otherwise, layoutChunk is populated, the current available area is updated, and the loop is iterated until the conditions are not met.

The population in the loop is implemented by layoutChunk.

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // Get the ViewHolder View from the cache to display the current position
        View view = layoutState.next(recycler);
        if (view == null) {
            // If we place the view in an obsolete view, this may return null, which means no more items need to be laid out.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            // Call the addView method according to the direction to add a child View
            if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0); }}else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
                // Here is the vanishing View, but need to set the corresponding removal animation
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0); }}// Call measure to measure the view. The padding of the parent class is taken into account
        measureChildWithMargins(view, 0.0);
        // Set the subview consumption area to the height (or width) of the subview.
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        // Find the four corners of the view
        intleft, top, right, bottom; .// Call the child.layout method for the layout (the view's ItemDecorator is taken into account)
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        // If the view has not been deleted or changed, the free space is used
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.isFocusable();
    }
Copy the code

There are five main things that are done here

  1. Get the View to display through layoutState
  2. Add child views to the layout using the addView method
  3. Call the measureChildWithMargins method to measure the sub-view
  4. Call layoutDecoratedWithMargins method View layout
  5. Based on the result of the processing, the LayoutChunkResult information is populated so that when returned, the data can be evaluated.

If we only consider the first data load, our entire page so far fills the entire screen with two fills.

Reuse mechanism

For RecyclerView reuse mechanism, we have to RecyclerView.Recycler. Its job is to recycle and reuse views. Recycler can quickly draw RecyclerView through Scrap, CacheView, ViewCacheExtension and RecycledViewPool.

Scrap

Scrap is the lightest cache in RecyclerView, it does not participate in the recycling of sliding, only as a temporary cache when relayout. Its purpose is to cache ViewHolder that appear on the screen both before and after the interface is rearranged, thus eliminating unnecessary reloading and binding.

When RecyclerView is reconfigured (not including the original RecyclerView, because there were no views on the screen when it was initialized), First call * * detachAndScrapAttachedViews () all the current View is displayed on the screen of the tag for the unit with the ViewHolder and recorded in the list, after the fill () * * in the process of filling the screen, The ViewHolder will be filled from the Scrap list first. The Contents of the ViewHolder returned directly from the Scrap remain unchanged and will not be recreated or bound.

The Scrap list exists in the Recycler module.

 public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null; . }Copy the code

As you can see, Scrap actually contains two ArrayLists of type ViewHolder. MAttachedScrap is responsible for saving the ViewHolder that will remain intact, while mChangedScrap is responsible for saving the ViewHolder that will be moved. Note that only the position is moved, but the contents remain intact.

This is a situation where mAttachedScrap and mChangedScrap are temporarily stored in a RecyclerView when B is removed and **notifyItemRemoved()** is invoked. At this point, A is completely unchanged before and after deletion, and it will be temporarily added into mAttachedScrap. B is the one we’re going to remove, and it’s also going to be put into mAttachedScrap, but it’s extra marked REMOVED, and it’s going to be REMOVED later. C and D will move up after B is deleted, so they will be temporarily placed in mChangedScrap. E does not appear on the screen before this operation. It is not subject to Scrap. Scrap will only cache ViewHolder that has been loaded on the screen. On deleting, A,B,C, and D will all go to the Scrap. On deleting, A,C, and D will all come back.

The partial refresh of RecyclerView relies on the temporary cache of Scrap. We need to notify RecyclerView which positions have changed through notifyItemRemoved(), notifyItemChanged() and other methods. Thus, the RecyclerView can use Scrap to cache ViewHolder whose other contents have not changed while processing these changes, thus completing the local refresh. Note that if we use the **notifyDataSetChanged()** method to notify RecyclerView to update, it will flag all on-screen views as FLAG_INVALID, Instead of trying to use Scrap to cache ViewHolder that will come back later, throw it all into the RecycledViewPool and then have to go through the binding process all over again when you come back.

Scrap is only used as a temporary cache for layout, it has nothing to do with sliding cache, its detach and reattach are only temporary during layout. At the end of the layout, the Scrap list should be empty, its members should either be reconfigured or removed, and at the end of the layout process, there should be nothing left in either Scrap list.

What is a CacheView?

CacheView is a cache list, ViewHolder, that is responsible for recycling views that have just moved off the screen when the RecyclerView position changes.

 public final class Recycler {...final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        intmViewCacheMax = DEFAULT_CACHE_SIZE; . }Copy the code

We can see that In Recycler, mCachedView, like Scrap, is an ArrayList stored in ViewHolder. This means that it also caches the ViewHolder as a whole and does not need to go through the creation and binding process for reuse, leaving the contents unchanged. It also has a maximum number of caches, which is two by default.

CacheView.png

As you can see from the figure above, CacheView will cache views that have just become invisible. This caching takes place during a fill() call. Since fill() is called during both layout updates and slides, this scenario is repeated during slides and may occur during layout updates due to position changes. The fill () after turnover will eventually call recycleViewHolderInternal (), there will be a mCachedViews. The add (). As mentioned above, CacheView has a maximum number of caches, so what happens if the cache is exceeded?

void recycleViewHolderInternal(ViewHolder holder) {...if (forceRecycle || holder.isRecyclable()) {
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)) {
                    // Retire is the oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }
                    mCachedViews.add(targetCacheIndex, holder); }... }Copy the code

RecycleViewHolderInternal in * * () there is such a, if come in a new ViewHolder Recycler found cache, would exceed the maximum limit, It calls recycleCachedViewAt(0) to recycle the ViewHolder that was cached first into the RecycledViewPool and then McAchedviews.add ()** to add a new cache. That is, when we recycle RecyclerView, the Recycler can constantly cache invisible views into CacheView and replace ViewHolder in CacheView when we reach the CacheView limit. Throw them into the RecycledViewPool. If we keep sliding in the same direction, CacheView doesn’t really help us with efficiency, it just keeps caching ViewHolder that we’re sliding behind; If we slide up and down a lot, caching in CacheView is a good use because it doesn’t need to be created or bound to be reused.

RecycledViewPool

As mentioned earlier, RecycledViewPool is the ultimate Recycler when Srap and CacheView don’t want to recycle.

 public static class RecycledViewPool {
        private SparseArray<ArrayList<ViewHolder>> mScrap =
                new SparseArray<ArrayList<ViewHolder>>();
        private SparseIntArray mMaxScrap = new SparseIntArray();
        private int mAttachCount = 0;

        private static final int DEFAULT_MAX_SCRAP = 5;
Copy the code

We can be found in the RecyclerView RecycledViewPool, can see it is save the form and the above of a, CacheView is different, it is a SparseArray nested ArrayList to save the ViewHolder. RecycledViewPool RecycledViewPool RecycledViewPool RecycledViewPool RecycledViewPool RecycledViewPool RecycledViewPool RecycledViewPool You can use it to implement multiple lists showing different types of list items).

Different from the previous two, RecycledViewPool only recycles a ViewHolder object of the viewType, and does not save the content of the original ViewHolder. BindViewHolder() will be called to rebind as we described in **onBindViewHolder()**, rendering it as a new list item.

Also, RecycledViewPool has a maximum number of recycledViewPools, 5 by default. If the maximum number is not exceeded, Recycler tries to recycle ViewHolder into RecycledViewPool. It is worth mentioning that RecycledViewPool will only be differentiated according to ViewType, as long as the ViewType is the same, and even can be reused in multiple recyclerViews in general, as long as they set the same RecycledViewPool.

In general, RecyclerView focuses on optimizing the performance by using cache and recycle reuse in two scenarios. First, when data is updated, Scrap is used to implement partial update, and useless reconstruction and binding of views that have not been changed can be reduced as much as possible. The second is to reuse the ViewHolder that has already been slid during a quick slide to minimize the stress of recreating the ViewHolder. The general idea is: reuse as long as it doesn’t change; Be lazy as long as you can without creating or rebinding.

Sliding handle

Before we dive into sliding, let’s explain a few variables of the LayoutState class.

  • MOffset: The offset of the starting position of the layout (mCoordinate set inside the anchor point)
  • MAvailable: The pixel value that can be filled in the layout direction, i.e. the free area
  • MScrollingOffset: The distance to scroll without creating a new view. Let’s say I have a View that shows half of the top half, so if I swipe up half of the way, I don’t need to create a new child View. So this mScrollingOffset is the maximum distance I can slide without creating a view.

Data processing after sliding

When scrolling occurs, the scrollHorizontallyBy method is triggered

//LinearLayoutManager.java
		public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            return 0;
        }
        return scrollBy(dx, recycler, state);
    }

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        // The tag is scrolling
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        // Confirm the scrolling direction
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        // Update layoutState to update its display screen area, offset, etc. For example, when you slide up, there will be a blank area of dy distance at the bottom, and then you need to call fill to fill the area of dy distance
        updateLayoutState(layoutDirection, absDy, true, state);
        // Call fill to populate the view presented to the client
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); .// Record the distance of this scroll
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }
Copy the code

There are two main things that happen when you scroll

  1. Update the related attributes within layoutState with the updateLayoutState method.
  2. Call fill to populate the data.

To better understand the property relationships inside layoutState, let’s take a look at the implementation inside updateLayoutState.

	//LinearLayoutaManager.java
	private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {
        int scrollingOffset;
        if (layoutDirection == LayoutState.LAYOUT_END) {
            // Get the bottom View of the current display
            final View child = getChildClosestToEnd();
            // Set the offset at the bottom of the currently displayed subview (including the height of Decor)
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // Subtracting the RecyclerView height from the bottom anchor position leaves us with no new View drawn within the scrollingOffset
            //getEndAfterPadding=RecyclerView height -padding heightscrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding(); }...Copy the code

The comments in the method are very detailed.

With the basics of reuse and an understanding of these variables, let’s go back to Fill to understand how LLM caching works.

The View of recycling

Let’s first look at View recycling.

	//LinearLayoutManager.java
	int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
            // Key method ** reclaims the View that slides off the screen
            recycleByLayoutState(recycler, layoutState);
        }

    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if(! layoutState.mRecycle || layoutState.mInfinite) {return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            // Reclaim the view from the End
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
            // Start to reclaim the viewrecycleViewsFromStart(recycler, layoutState.mScrollingOffset); }}Copy the code

So here we’re going to do a thumb slip, recycleViewsFromStart. The other case is similar and can be understood by yourself

    // Retrieve the View from the header
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
        //limit indicates how much sliding is not drawn
        final int limit = dt;
        // Returns the number of current child views attached to the superview
        final intchildCount = getChildCount(); .// Iterate over the subview
            for (int i = 0; i < childCount; i++) {
                // Get the child View
                View child = getChildAt(i);
                // If the bottom position of the current View is >limit, then there is a View to draw, and the top View needs to be reclaimed
                // The logic here is that if the bottom View does not need to be drawn, then the top View will not be recycled
                if(mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit)  { recycleChildren(recycler,0, i);
                    return; }}}}Copy the code

It will be followed and finally enter

    //LinearLayoutManager.java
	private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {... removeAndRecycleViewAt(i, recycler); . }}//RecyclerView.java
    public void removeAndRecycleViewAt(int index, Recycler recycler) {
    	final View view = getChildAt(index);
    	removeViewAt(index);
    	recycler.recycleView(view);
    }
    
    //RecyclerView.java
        public void recycleView(View view) {
            recycleViewHolderInternal(holder);
        }
Copy the code

The ultimate recovery will through recycleViewHolderInternal method to perform the operation.

        void recycleViewHolderInternal(ViewHolder holder) {
            // Check for various unrecoverable cases.if (forceRecycle || holder.isRecyclable()) {
                // Meet the requirements of recycling
                if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    // The sliding view is first saved in mCachedViews
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        //mCachedViews can only cache mViewCacheMax and RecycledViewPool
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0  && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if(! mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    // Place the ViewHolder to mCachedViews
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if(! cached) {// If it is already cached. So it won't be executed here.
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true; }}... mViewInfoStore.removeViewHolder(holder);if(! cached && ! recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView =null; }}Copy the code

This part of the code belongs to the process of View recycling when sliding

  1. If not, an exception is thrown
  2. If the recycle condition is met, it will check whether cachedViewSize is full. If it is full, it will remove the earliest one to RecycledViewPool, and then add the current one to cachedViewSize. If it’s not full, it goes straight in.
  3. The storage thread pool RecycledViewPool will be cached to different queues according to the ViewType. Each queue type can be cached up to 5 times. If it is full, it is no longer cached.

The reuse of the View

The fill method not only contains the recycling of the View that slides off the screen, but also uses the ViewHolder to reuse the interface that will be presented for quick processing. Calls to recycle are triggered by LayoutState.next (Recycler) in layoutChunk.

		//LinearLayoutManager.java
        void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
            // Get the ViewHolder View from the cache to display the current position
            View view = layoutState.next(recycler);
		//LinearLayoutManager.java        
        View next(RecyclerView.Recycler recycler) {...finalView view = recycler.getViewForPosition(mCurrentPosition); . }//RecyclerView.java
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }
		//RecyclerView.java
        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }
Copy the code

Finally for the ViewHolder reuse logic is by tryGetViewHolderForPositionByDeadline to deal with.

//RecyclerView.java
	ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // Try to get it from mChangedScrap. This logic is used when the location of the data changes. A notifyItemRemove() moves the following data up, using this logic
            if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! =null;
            }
            if (holder == null) {
                // Try to get it from mAttachedScrap, Hidden list, mCachedViews according to position
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if(holder ! =null) {
                    // Verify that the holder obtained is valid. Illegal, holder will be recycled. If valid, the fromScrapOrHiddenOrCache flag is true. Indicates that the holder is fetched from the cache.
                    if(! validateViewHolderForOffsetPosition(holder)) { ... holder =null;
                    } else {
                        fromScrapOrHiddenOrCache = true; }}}if (holder == null) {...if (mAdapter.hasStableIds()) {
                    // Try to get mAttachedScrap and mCachedViews respectively according to id
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
                    if(holder ! =null) {
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true; }}// Try to fetch it from our custom mViewCacheExtension
                if (holder == null&& mViewCacheExtension ! =null) {
                    final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type);
                    if(view ! =null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder");
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view."); }}}if (holder == null) {
                    // From the cache pool
                    holder = getRecycledViewPool().getRecycledView(type);
                    if(holder ! =null) {
                        holder.resetInternal();
                        if(FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); }}}if (holder == null) {
                    long start = getNanoTime();
                    if(deadlineNs ! = FOREVER_NS&& ! mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {return null;
                    }
                    // Create a ViewHolder by calling Adatper's createViewHolder method if still not available
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if(innerView ! =null) {
                            holder.mNestedRecyclerView = newWeakReference<>(innerView); }}longend = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); }}...boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                // Data does not need to be bound (mChangedScrap, mAttachedScrap cache Holder does not need to be rebound)
                holder.mPreLayoutPosition = position;
            } else if(! holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {// The holder does not bind data, or needs to be updated or the holder is invalid
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"+ " come here only in pre-layout. Holder: " + holder);
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                // There will be data bindingbound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); }...return holder;
        }
Copy the code

The logic of this section is the entire reuse process of our ViewHolder. You can summarize it

  1. Get it from mChangedScrap
  2. Try to get it from mAttachedScrap, hidden list, mCachedViews ** according to position
  3. Try to get it from mAttachedScrap and mCachedViews (level 1 cache)** according to ID
  4. Try to get it from our custom **mViewCacheExtension **
  5. Fetch from the cache pool according to ViewType
  6. Create a ViewHolder using createViewHolder of adapter if none of the above is available

Once we get the ViewHolder we’re going to need to bind, which is to show our data in the View. If get the ViewHolder cache is data binding, you don’t need to, otherwise you need to pass tryBindViewHolderByDeadline method calling adapter bindViewHolder for data binding.

conclusion

The whole article ended here, relatively speaking, the content is more. First from the measurement of RecyclerView as the entrance, in the process of measurement, mentioned the reuse mechanism. Finally, through the process of RecyclerView sliding method, from the source code level explained the Holder recycling and reuse mechanism.

Summary of the source code analysis to learn the new knowledge:

  1. In RecyclerView, internal Recycler is responsible for recycling and reuse.
  2. When sliding RecyclerView, fill is constantly called to determine whether it needs to be filled.
  3. The LinearLayoutManager can be set to reverse traverse the layout with setReverseLayout. The first item is placed at the end of the UI, and the second item is placed before it.
  4. When the View slides back, the adapter’s onViewDetachedFromWindow method is called back
  5. Deeply understand the cache mechanism and principle of RecyclerView
  6. The layout process and principle of RecyclerView sub-view are explained

This article is published by Kaiken!

Synchronous public number [open Ken]