RecyclerView is one of the components we use a lot in Android development to display a lot of data. We should not only be proficient in using it, but also have a sense of its implementation. This article introduces the RecyclerView drawing process, that is, onMeasure, onLayout, onDraw these three methods mainly do what work, let’s go!

OnMeasure

We know that the method is to measure the width and height of the current View and child View, but look at the source code of RecyclerView found that the code is very long, it is not only done to measure the work, but also do some other work, let’s take a look

if (mLayout == null) {
   // The first case
}
if (mLayout.isAutoMeasureEnabled()) {
  // The second case is usually entered in this case
}else{
 // The third case
}
Copy the code

The onMeasure method is divided into three cases based on conditional branching, which we will discuss one by one

  • Case 1:mLayout == null
if (mLayout == null) {
    defaultOnMeasure(widthSpec, heightSpec);
    return;
}
void defaultOnMeasure(int widthSpec, int heightSpec) {
    final int width = LayoutManager.chooseSize(widthSpec,getPaddingLeft() + getPaddingRight(),ViewCompat.getMinimumWidth(this));
    final int height = LayoutManager.chooseSize(heightSpec,getPaddingTop() + getPaddingBottom(),ViewCompat.getMinimumHeight(this));
    setMeasuredDimension(width, height);
}
public static int chooseSize(int spec, int desired, int min) {
    final int mode = View.MeasureSpec.getMode(spec);
    final int size = View.MeasureSpec.getSize(spec);
    switch (mode) {
    case View.MeasureSpec.EXACTLY:
        return size;
    case View.MeasureSpec.AT_MOST:
        return Math.min(size, Math.max(desired, min));
    case View.MeasureSpec.UNSPECIFIED:
        default:
        returnMath.max(desired, min); }}Copy the code

When mLayout == NULL, it can be seen from the incoming parameters that RecyclerView determines its size without considering the sub-view. It is a relatively rough measurement, and the specific size also needs to be determined according to the results of several subsequent measurements

  • Case 2mLayout.isAutoMeasureEnabled() == true

MLayout. IsAutoMeasureEnabled () is true, will be called LayoutManager. OnLayoutChildren (Recycler, State) to calculate the size of the View are needed, RecyclerView. The implementation class LayoutManager LinearLayoutManager, StaggeredGridLayoutManager overloading and the proposed method returns true, so usually goes into this branch (the listed part of the code)

if (mLayout.isAutoMeasureEnabled()) {
    // Pass the measurement to LaytouManager
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    final boolean measureSpecModeIsExactly =
    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
    // If width and height are already exact values, then the measurement is no longer required
    if (measureSpecModeIsExactly || mAdapter == null) {
        return;
    }
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
    }
    dispatchLayoutStep2();
    // If a secondary measurement is required
    if(mLayout.shouldMeasureTwice()) { dispatchLayoutStep2(); }}Copy the code

The first step calls the layoutManager.onMeasure () method

public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
    mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
Copy the code

This method directly calls the default measurement method of RecyclerView, which is the first case we analyzed before. Reading the method’s comments, LayoutoManager strongly recommends turning on automatic measurement, and does not override the method if it is enabled, and the three default implementations of LayoutoManager do not override the method. The measurement strategy is also introduced. If the width and height measurement mode is UNSPECIFIED. AT_MOST, specify EXACTLY and RecyclerView occupies the maximum available space.

MeasureSpec.EXACTLY or if Adapter is not set.

STEP_START is the default value of mstate.mlayoutstep. Go to the conditional statement and execute dispatchLayoutStep1(), then dispatchLayoutStep2(), If a secondary measurement is required perform a dispatchLayoutStep2().

We focus on dispatchLayoutStep1() and dispatchLayoutStep2(). A variable closely related to these two methods is mstate.mlayoutstep. The significance of this variable is to determine dispatchLayoutStep1() and DISP AtchLayoutStep1 () and dispatchLayoutStep2() which step should be performed? It has three values

mLayoutStep describe
STEP_START Default value ordispatchLayoutStep3()It has been executed
STEP_LAYOUT dispatchLayoutStep1()It has been executed
STEP_ANIMATIONS dispatchLayoutStep2()It has been executed

For the specific analysis of these three methods, we put onLayout processing, first say a conclusion dispatchLayoutStep1() to deal with the Adapter data update and prepare the animation before the data; DispatchLayoutStep2 () for itemView layout

  • Situation three: mLayout isAutoMeasureEnabled () = = false

    We usually use layoutManagers that return true unless we customize them, so we won’t analyze this case for now

To summarize what onMeasure does, assume that the LayoutManager is a LinearLayoutManager

  1. Measure yourself. It may take several measurements
  2. If not both width and heightMeasureSpec.EXACTLYMode executes
  • dispatchLayoutStep1()To deal withAdapterUpdate and prepare the data before animation
  • dispatchLayoutStep2()Layout the itemView

OnLayout

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }
Copy the code

That’s dispatchLayout

void dispatchLayout(a) {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if(mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) {// First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }
Copy the code

The process is clear

  • ifAdapterandLayoutManagerNo layout without setting,RecyclerViewIt’s just going to be blank
  • If beforeonMeasureTo perform thedispatchLayoutStep1()anddispatchLayoutStep2()These two methods are no longer executed, howeverdispatchLayoutStep2()It may need to be called again
  • performdispatchLayoutStep3()

Now what does the dispatchLayoutStep family of methods do

  • dispatchLayoutStep1()
/** * The first step of a layout where we; * - process adapter updates * - decide which animation should run * - save information about current views * - If necessary, run predictive layout and save its information */
    private void dispatchLayoutStep1(a) {
        processAdapterUpdatesAndSetAnimationFlags();
        if (mState.mRunSimpleAnimations){
            / /...
        }
        if (mState.mRunPredictiveAnimations){
            / /...
        }
        mState.mLayoutStep = State.STEP_LAYOUT;
    }
Copy the code

DispatchLayoutStep1 () handles Adapter updates and the data needed to prepare the animation. While processAdapterUpdatesAndSetAnimationFlags () is used to handle Adapter updates and animation Flag processing, we take a look at this method

private void processAdapterUpdatesAndSetAnimationFlags(a) {
    if (mDataSetHasChangedAfterLayout) {
            // Processing these items have no value since data set changed unexpectedly.
            // Instead, we just reset it.
            mAdapterHelper.reset();
            if (mDispatchItemsChangedEvent) {
                mLayout.onItemsChanged(this); }} for the animation of the flag assignment mState. MRunSimpleAnimations =... mState.mRunPredictiveAnimations = ... }Copy the code

First deal with the renewal of the Adapter (Adapter. NotifyDataSetChanged () or RecyclerView. SwapAdapter (Adapter, Boolean) represent Adapter have update). It simply resets the operation of each item recorded previously. Because the data set changes, the information stored previously is meaningless. The following code assigns the flag bit of the animation. We call RecyclerView. SetAdapter and Adapter. NotifyDataSetChanged () will not trigger the animation, so we won’t consider animation related things.

Let’s move on to dispatchLayoutStep1(), and here are two if conditions, Involving two variables, mState. MRunSimpleAnimations and mState. MRunPredictiveAnimations only when the two variables in to perform the animation to true, so don’t consider the content inside. Mstate.mlayoutstep = state.step_layout, dispatchLayoutStep1() is done

Summary dispatchLayoutStep1() handles the data required for Adapter updates and preparing animations

  • dispatchLayoutStep2()
private void dispatchLayoutStep2(a) {
    mLayout.onLayoutChildren(mRecycler, mState);
    mState.mLayoutStep = State.STEP_ANIMATIONS;
}
Copy the code

Method is very concise, calls the LayoutManager. OnLayoutChildren (Recycler Recycler, State State), the method is of the essence of the sub View layout method, but is an empty implementation, subclasses must implement this method, the statement is as follows

public void onLayoutChildren(Recycler recycler, State state) {
    Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, Statestate) ");
}
Copy the code

Mstate.mlayoutstep = state.step_animations Means dispatchLayoutStep2() has completed

Conclusion dispatchLayoutStep2 () call LayoutManager. OnLayoutChildren for the layout of the View

  • dispatchLayoutStep3()
private void dispatchLayoutStep3(a) {
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
    // Record the View information after the layout and trigger the animation
    / /...
    }
    / /... Some cleaning up
}
Copy the code

Will first mState. MLayoutStep = State. STEP_START, mark dispatchLayoutStep3 () has been carried out, and then mState. MRunSimpleAnimations this variable indicates whether or not the animation, You don’t need animation for the first layout so you’re not going to go into this branch, and we’ll talk about animation later, and do some cleaning up at the end.

Summary dispatchLayoutStep3() trigger animation

Summarize the work done by onLayout, the general process is as follows

  1. dispatchLayoutStep1()The Adapter update and the data needed to prepare the animation were processed
  2. dispatchLayoutStep2()Call LayoutManager. OnLayoutChildren for the layout of the View
  3. dispatchLayoutStep3()Trigger the animation

draw

    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
     / /... Handle clipToPadding="false"
    }
Copy the code

onDraw

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState); }}Copy the code

Conclusion: draw and onDraw draw the RecyclerView dividing line, of course, dividing line needs us to achieve the specific drawing content, at the same time, we also know the difference between onDraw and onDrawOver in ItemDecoration

So far we have completed the source code analysis of RecyclerView three processes, most of the code listed above has been simplified, leaving out a lot of details, but just began to read the source code, as long as we grasp the overall process is good, put aside the details, the above overall process is not difficult to understand. But there is a very important method is not fine, is LayoutManager. OnLayoutChildren (), the method is the sub is the core of the View layout, we analysis on the method for a single wave to LinearLayoutManager (only consider the vertical) as an example to look over

LinearLayoutManager.onLayoutChildren()

We need to take a look at the helper classes that are used to help with the layout of subviews

  • LayoutState
attribute explain
mOffset How many pixels will be offset before the layout begins
mAvailable The space available in the current layout direction
mCurrentPosition You want to lay out where the child views are represented in the Adapter
mInfinite There is no limit to the number of views a layout can have
  • AnchorInfo
attribute explain
mPosition The anchor position
mCoordinate Anchor point coordinate information
mValid Whether the available
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 a LayoutState
    ensureLayoutState();
    if(! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! =null) {
            mAnchorInfo.reset();
            // Update anchor information
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
    }
    / /... Calculate the extra space required for the LinearLayoutManager
    // The anchor information is ready
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    // Cache all existing child views
    detachAndScrapAttachedViews(recycler);
    if (mAnchorInfo.mLayoutFromEnd){
        / /...
    }else{
        // fill towards end
        // Save anchor information to mLayoutState
        updateLayoutStateToFillEnd(mAnchorInfo);
        fill(recycler, mLayoutState, state, false);
        // fill towards start
        updateLayoutStateToFillEnd(mAnchorInfo);
        fill(recycler, mLayoutState, state, false);
        
        if (mLayoutState.mAvailable > 0) {
            fill(recycler, mLayoutState, state, false); }}}Copy the code

This code omits a lot of useful information, including assigning some useful attributes inside LayoutState. The comments at the beginning of the code provide insight into the internal execution logic of the method

  1. Locate anchor position and anchor coordinates
  2. Starting at the anchor point, populate the layout child View upward until the area is filled
  3. Starting at the anchor point, populate the layout subview downward until the area is filled
  4. Roll to meet requirements, such as stacking from the bottom

The key is the anchor point. For the LinearLayoutManager, it may not necessarily start the layout from the highest or lowest point, but it may start the layout from some point in the middle, as shown in the figure

RecyclerView

  • Determine anchor point information
    1. The first layout, anchor information is definitely not available, into the update anchor conditional statement, inside the callupdateAnchorInfoForLayout(recycler, state, mAnchorInfo)Update anchor information
    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
        // The first calculation method
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from pending information");
            }
            return;
        }
        // The second calculation method
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from existing children");
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "deciding anchor info for fresh state");
        }
        // The third calculation method
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
Copy the code

You can see that there are three ways to calculate anchor information, each of which has a lot of code but is not difficult to understand

  • Methods a
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
        if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        / /...
}
Copy the code

MPendingScrollPosition == recyclerView. NO_POSITION Other values are available only when scrollToPosition is called or the mPendingScrollPosition recorded before onRestoreInstanceState is restored So by default this method does not calculate the anchor information, go down

  • Method 2updateAnchorFromChildrenThis method determines anchor information based on the subview
    • If there are no child views, return directlyfalse, indicates that the anchor point information is not calculated
    • If there is a child View, the anchor is usually the position of the child View visible on the screen. I’m going to take the first visible View on the screen, the child View of Positon =1,anchorInfo.mCoordinateThe Decor top location assigned to subview 1

The method of detailed analysis can see this article RecyclerView source code analysis

  • Methods three

MCoordinate = 0 if the padding is not set, Anchorinfo. mPosition = 0(mStackFromEnd == false, which defaults to false)

Anchor point information is calculated mainly to assign values to the two variables mPosition and mCoordinate, so that we know which point to start filling the sub-view and the position of the data corresponding to the sub-view in the Adapter

After updating the anchor point information, there is a long section of code in the source code to calculate the “extra space” required by the LinearLayoutManager. This code also does not understand, so I will not analyze it, but it does not affect our grasp of the overall layout process. Anchor point information is ready, updateLayoutStateToFillEnd () to save anchor point information to the mLayoutState, then call the fill () method to fill the child View, mAnchorInfo. MLayoutFromEnd filling can be divided into two cases

  • trueFrom:AdapterLet’s start with the last one and go from the bottom up
  • falseFrom:AdapterStarting with the first item, the layout from top to bottom (the default) looks like this (dotted lines indicate the ones outside the screenItemView)

The default is false, starting the layout from the top down and then entering the key fill() method

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    // Free space
    final int start = layoutState.mAvailable;
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    // Save the space consumed by each child View
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // Loop layout subviews
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            // Core method
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if(! layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList ! =null| |! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed;// we keep a separate remaining space because mAvailable is important for recycling
                // Consume the space occupied by the child View
                remainingSpace -= layoutChunkResult.mConsumed;
            }
         / /...
    }      
return start - layoutState.mAvailable;
}
Copy the code

The core idea of fill is to continuously arrange subviews in a loop. LayoutChunk is responsible for filling when there is no available space or data from the data source. For each subview, the remaining space is subdivided by the space occupied by the corresponding View (vertically, that is, the height), and then the next one is filled. Finally, the size of the area filled in the layout is returned.

Let’s go into layoutChunk to see the implementation in action

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
        // Get a child View
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        // Add the View to RecyclerView
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0); }}else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0); }}// Measure the size of sub-views including the margins and decorations
        measureChildWithMargins(view, 0.0);
        // Save the occupied space in LayoutChunkResult for the outer layer to recycle
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else{ top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; }}else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else{ left = layoutState.mOffset; right = layoutState.mOffset + result.mConsumed; }}// We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        // Put the child View in the appropriate position
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

Copy the code
  • The first stepView view = layoutState.next(recycler)To get aitemViewAnd here it isRecyclerViewCaching mechanisms are discussed in a later chapter.
  • The second step is to goitemViewAdded to theRecyclerView, divided into two cases
    • addViewIt makes sense to call the method after making some logical judgments about correct placement, avoiding repeated additions, and so onViewGrouptheaddViewTo implement.
    • addDisappearingViewRepresents that the View is about to disappear from the screen, such as an underline or callAdapter.notifyItemRemoved, the method and the aboveaddViewAll calls are internaladdViewInt(View child, int index, boolean disappearing), but the last parameter is different.
  • Step 3 MeasurementitemViewThe size of themeasureChildWithMargins(view, 0, 0)Inside this method, there are other considerations besides its own sizemarginanddecorationsThe size of a dividing line. After measuring, save the consumed space toLayoutChunkResultFor the outer layer to recycle.
  • The fourth step willitemViewPut it in the right place, when you calculate the positionlayoutState.mOffsetIt depends on the coordinates of the anchor points that we calculated earlier, if it’s the first oneitemView,layoutState.mOffsetThe coordinates are the same as the anchor points, and you can debug to see the data correspondence. Of course, the layout was also consideredmarginanddecorations(What we call a dividing line)

Above will fill () method of analysis is completed, LinearLayoutManager. OnLayoutChildren core is analyzed, and finally a layoutForPredictiveAnimations, from the perspective of the annotation of the method, It is for the purpose of animation to do some layout, and it is not necessary to execute, so I will not analyze it, if there are readers who know this content, I hope you can let me know.

. At this point, LinearLayoutManager onLayoutChildren analysis is completed, but the method annotation of the last article, posted on the original 4) scroll to fulfill requirements like stack from bottom. I don’t see where it is reflected, maybe it is included in some details omitted above. In short, I don’t understand this point, if there are readers who are clear, I hope you can tell me.

conclusion

RecyclerView drawing process we finished the analysis, summarize

  1. onMeasurewithLayoutManagerWhether or not automatic measurement is enabled is dependent, if automatic measurement is supported, it may be pre-layout, the default implementation of threeLayoutManagerAll support automatic measurement, if customLayoutManagerPay attention to that
  2. onLayoutMainly in thedispatchLayoutStep1(),dispatchLayoutStep1(),dispatchLayoutStep1()These three methods are called sequentially, with the first and third dealing mainly with animation, and the second handing over layout tasksLayoutManager
  3. drawandonDrawCall theItemDecorationWe implement these methods from the defined splitter line

Finally, due to the author’s limited level, if there is any mistake in the above analysis, welcome to put forward, I will correct it in time, so as not to mislead others

The relevant data

RecyclerView source code analysis (a) – RecyclerView three processes

RecyclerView source code analysis

RecyclerView analysis