“This is the 21st day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021”

preface

The drawing framework of RecyclerView was introduced in the previous article, and the specific drawing work of RecyclerView and its sub-views was realized by onLayoutChildren and setMeasuredDimension in the specific LayoutManager.

As a component of RecyclerView, LayoutManager is responsible for item layout drawing and item recycling. The former is what we will comb through in this article, while the latter involves sliding-related content, which will comb through the interaction line. LayoutManager is an abstract class, the system provides the inherit its LinearLayoutManager GridViewLayoutManager, StaggeredGridLayoutManager three LayoutManager. So let’s first look at how the LinearLayoutManager implements drawing.

implementation

onLayoutChildren

@Override 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. ... ensureLayoutState(); mLayoutState.mRecycle = false; // resolve layout direction resolveShouldLayoutReverse(); . if (! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! = null) { mAnchorInfo.reset(); mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate updateAnchorInfoForLayout(recycler, state, mAnchorInfo); mAnchorInfo.mValid = true; }... if (mAnchorInfo.mLayoutFromEnd) { ... } else { // fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtraFillSpace = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; final int lastElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForStart += mLayoutState.mAvailable; } // fill towards start updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtraFillSpace = extraForStart; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; if (mLayoutState.mAvailable > 0) { extraForEnd = mLayoutState.mAvailable; // start could not consume all it should. add more items towards end updateLayoutStateToFillEnd(lastElement, endOffset);  mLayoutState.mExtraFillSpace = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; }}... }Copy the code

In terms of layout, onLayoutChildren gives the implementation algorithm at the beginning of the comment:

  • 1 Locate the anchor position and coordinates according to the child control and some variables
  • Fill the child control from the anchor position
  • 3 slide to the desired position (this article focuses on the first two steps; the third step will be combed out in the interaction section).

I’ve partially ignored the onLayoutChildren code to make the structure look clearer. Let’s look at the first step, identify the anchor point.

Determine the anchor

The anchor point, in this case, is the item that is positioned first. The information about the anchor point is represented by the AnchorInfo class in the LinearLayoutManager

static class AnchorInfo { OrientationHelper mOrientationHelper; Int mPosition; int mPosition; // Location of item corresponding to anchor int mCoordinate; // The distance to the top of the item position corresponding to the anchor Boolean mLayoutFromEnd; // Whether to layout from bottom up. In the scenarios discussed in this article, the value is false Boolean mValid; // Anchor information is set... }Copy the code

LinearLayoutManager in determining the method of anchor is updateAnchorInfoForLayout (), HTML code is as follows, updateAnchorInfoForLayout anchor information by three kinds of judgment, Begin from updateAnchorFromPendingData () to obtain an information, capturing the if, direct return; If not, retrieve anchor information from updateAnchorFromChildren(). If so, return anchor information directly. If the first two methods do not get anchor information, the code will go to the last two lines to get the mCoordinate and mPosition of the Anchor.

private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
        AnchorInfo anchorInfo) {
    if (updateAnchorFromPendingData(state, anchorInfo)) {
        ...
        return;
    }
    if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
        ...
        return;
    }
    anchorInfo.assignCoordinateFromPadding();
    anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
Copy the code

UpdateAnchorFromPendingData and updateAnchorFromChildren have occurred in the item view, the former is related to a sliding judgment is mPendingScrollPosition, This value is set by scrollToPosition(), which will regard mPendingScrollPosition as the mPosition of anchor, and then obtain mCoordinate according to the corresponding item View of mPosition. When discussing sliding in the follow-up, Will specify; GetFocusedChild () and isViewValidAsAnchor() are used to find the focus child control that meets the requirements of the anchor point. If it does not exist, Through findReferenceChildClosestToStart starting position () to find the nearest child controls, to find, after call assignFromView set related information of the anchor.

private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) { ... final View focused = getFocusedChild(); if (focused ! = null && anchorInfo.isViewValidAsAnchor(focused, state)) { anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); return true; }... View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state); if (referenceChild ! = null) { anchorInfo.assignFromView(referenceChild, getPosition(referenceChild)); . return true; } return false; }Copy the code

If updateAnchorFromPendingData () and updateAnchorFromChildren () returns false, not to an information. In this case, will implement the third way to get an information, through assignCoordinateFromPadding (), set the value of mCoordinate, this article discusses the scenario, Value of mOrientationHelper. GetStartAfterPadding (), mPosition a value of 0.

anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
Copy the code

Now that you have the anchor information, the next step is to populate the child controls.

Fill child control

The key code to fill the child control, fill(), is as follows. As you can see, the child control is filled through a while loop that ends when there is no space available or no child controls to fill.

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        ...
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        /**
         * Consume the available space if:
         * * layoutChunk did not request to be ignored
         * * OR we are laying out scrap children
         * * OR we are not doing pre-layout
         */
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        ...     
    }
    return start - layoutState.mAvailable;
}
Copy the code

The core code in Fill () is layoutChunk(), which concretely implements the measurement and layout of child controls in layoutChunk().

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); if (view == null) { ... result.mFinished = true; return; } 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 { ... } measureChildWithMargins(view, 0, 0); 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. layoutDecoratedWithMargins(view, left, top, right, bottom); . // 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

LayoutChunk does the following:

First, get the child view to be laid out

To obtain recyclable views, LayoutState’s Next () can be used internally by Next () to obtain recyclable views through the method of getViewForPosition(). The subsequent analysis will be conducted in detail when recyclable is recyclable. After obtaining the child view, add it to the parent container RecyclerView using addView() method.

Second, measure subview

MeasureChildWithMargins (), measureChildWidthMargins() removes the padding, margin, and Decoration from the margins, and assigns the remaining dimensions to the child view as the parent container. The subview is passed through the measure() method to start the measurement of the subview.

public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, canScrollHorizontally()); final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, canScrollVertically()); if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); }}Copy the code

Third, layout sub-view

Is used layout layoutDecoratedWithMargins () method, you can see calls into the layout () method, here into the layout of the view.

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
        int bottom) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect insets = lp.mDecorInsets;
    child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
            right - insets.right - lp.rightMargin,
            bottom - insets.bottom - lp.bottomMargin);
}
Copy the code

At this point, the subview is how to measure the layout, comb through. Back to the fill() method, let’s look at a few criteria to end the while loop:

First, remainingSpace is less than or equal to 0,

int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
while (…) {
  ...
  remainingSpace -= layoutChunkResult.mConsumed;
  ...
}
Copy the code

Initial value is layoutState remainingSpace mAvailable + layoutState. MExtraFillSpace, here, MAvailable value is updateLayoutStateToFillStart ()/updateLayoutStateToFillEnd (), specific code in onLayoutChildren (), as for concrete determined by which method, There are several cases. The first is based on the mLayoutFromEnd in the anchor information, which is usually false, meaning the layout is done from scratch. In this case, will begin with anchor, fill in the corresponding item of anchor points at the back of the child controls, call updateLayoutStateToFillEnd () set the various properties of mLayoutState, among them include mAvailable; And then fill the child controls in front of the anchor, call updateLayoutStateToFillStart () sets the mLayoutState various attributes.

private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) { updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate); } private void updateLayoutStateToFillEnd(int itemPosition, int offset) { mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; . } private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) { updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate); } private void updateLayoutStateToFillStart(int itemPosition, int offset) { mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); . }Copy the code

MExtraFillSpace and sliding correlation, in order to more smooth sliding, at the time of sliding, can assignment mOrientationHelper mExtraFillSpace getTotalSpace (), the purpose is to additional fill a page view. In other cases, this value is 0.

Every time after entering into a while loop body, remainingSpace can subtract layoutChunkResult mConsumed, layoutChunkResult. MConsumed in layoutChunk assignment in (), Value of mOrientationHelper. GetDecoratedMeasurement (view).

Layoutstate.hasmore (state) is false.

boolean hasMore(RecyclerView.State state) {
    return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}
Copy the code

The initial value of mCurrentPosition is the mPosition corresponding to the anchor point. Each time LayoutState. next(Recycler) retrievalView is +1/-1 depending on the filling direction.

View next(RecyclerView.Recycler recycler) {
    ...
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}
Copy the code

After three, call layoutChunk (), if layoutChunkResult mFinished to true, means that there is no need to fill the child controls, then perform out of the while loop operation.

setMeasuredDimension

As can be seen from the above, setMeasuredDimension is used to deal with both the length and width of RecyclerView and the wrAP_content in this case. RecyclerView measuredWidth/measuredHeight length/width is measured by the child controls are the biggest decision.

void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
    ...
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        final Rect bounds = mRecyclerView.mTempRect;
        getDecoratedBoundsWithMargins(child, bounds);
        if (bounds.left < minX) {
            minX = bounds.left;
        }
        if (bounds.right > maxX) {
            maxX = bounds.right;
        }
        if (bounds.top < minY) {
            minY = bounds.top;
        }
        if (bounds.bottom > maxY) {
            maxY = bounds.bottom;
        }
    }
    mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
    setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
}
Copy the code

The LinearLayoutManager does not override setMeasuredDimension(), but uses setMeasuredDimension() of LayoutManager.

public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
    int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
    int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
    int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
    int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
    setMeasuredDimension(width, height); 
}
Copy the code

conclusion

This article combs the LinearLayoutManager drawing related code. LayoutManager carries the child control drawing in RecyclerView (the content of this article), the recycling of child control reuse, sliding related logic and optimization. It was a pain to write because there was so much riding on it and all the code was tangled up and I wanted to make every line as clean as possible. It’s not too long, but it takes a long time. Hopefully, the drawing part of LLM will be clarified.

The price of flexibility is complexity