RecyclerView source code analysis LinearLayoutManager drawing

preface

The previous article introduced the rendering framework of RecyclerView and learned that the specific rendering of RecyclerView and its sub views is realized through onLayoutChildren and setMeasuredDimension in LayoutManager.

As a component of RecyclerView, LayoutManager is responsible for the layout drawing of items and the recycling and reuse of items. The former is the content to be combed in this article, and the latter involves sliding related content, which will be combed on the interaction line. LayoutManager is an abstract class. The system provides three layoutmanagers that inherit it: LinearLayoutManager, GridViewLayoutManager and StaggeredGridLayoutManager. First, let's take a look at how LinearLayoutManager implements drawing.

realization

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;
        }
    }
    ...
}

As for how to layout, onLayoutChildren gave the implementation algorithm in the comments at the beginning:

  • 1 find the anchor position and coordinates according to the sub controls and some variables
  • 2 fill the child controls from the anchor position
  • 3 slide to the position that meets the requirements (this paper focuses on the first two steps, and the third step will be combed in the interaction part.)

I partially ignored the code of onLayoutChildren to make the structure look clearer. Let's look at the first step to determine the anchor point.

Determine anchor point

The so-called anchor point here refers to the item located first. The anchor point related information is represented by AnchorInfo class in LinearLayoutManager

static class AnchorInfo {
    OrientationHelper mOrientationHelper; //Auxiliary class, which is used to obtain data related to the item view layout
    int mPosition; //item location corresponding to anchor
    int mCoordinate; //Distance from the item position corresponding to anchor to the top
    boolean mLayoutFromEnd; //Whether to layout from bottom to top, in the scenario discussed in this article, the value is false
    boolean mValid; //Whether the anchor information has been set
    ...
}

The method to determine the anchor point in the LinearLayoutManager is updateAnchorInfoForLayout(). The code is as follows. updateAnchorInfoForLayout obtains the anchor information through three judgments. First, obtain the anchor information from updateAnchorFromPendingData(). If it is obtained, it will be returned directly; If it is not obtained, then obtain the anchor information from updateAnchorFromChildren(). If it is obtained, it is also returned directly; If neither of the first two methods obtains the anchor information, the code will go to the last two lines to obtain the anchor's mCoordinate and mPosition.

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;
}

Both updateAnchorFromPendingData and updateAnchorFromChildren occur when there is an item view. The former is related to sliding. The judgment is based on mPendingScrollPosition. This value is set by scrollToPosition(). It will take mPendingScrollPosition as the anchor's mPosition, and then use the item corresponding to mPosition   View gets mCoordinate, which will be explained in detail when sliding is discussed later; The latter is to get anchor through child controls. The code is as follows: first, getFocusedChild() and isViewValidAsAnchor() are used to find the focus sub control that meets the anchor requirements. If it does not exist, then findReferenceChildClosestToStart() is used to find the nearest sub control that leaves the initial location. After that, assignFromView is called to set the anchor information.

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;
}

If both updateAnchorFromPendingData() and updateAnchorFromChildren() return false, the anchor information is not obtained. In this case, the third method will be used to obtain the anchor information. Set the value of mCoordinate through assignCoordinateFromPadding(). In the scenario discussed in this article, the value is marientationhelper. Getstartafterpadding(), and the value of mPosition is 0.

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

Now that the anchor information has been obtained, the next step is to fill in the child controls.

Fill child controls

The key code fill() for filling in child controls is as follows. You can see that the child controls are filled through the while loop. The end condition is that there is no available space 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;
}

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

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();
}

layoutChunk does the following:

1, Get the sub view to be laid out

Specifically, the child view to be deployed is obtained through the next() method of LayoutState, and the view is obtained by using the getViewForPosition() method of Recycler in the next() method. When the Recycler is analyzed later, it will be analyzed in detail. After obtaining the child view, use the addView() method to add it to the parent container RecyclerView.

2, Measurement sub view

Embodied in the measureChildWithMargins() method, measureChildWidthMargins() removes the padding, margin and Decoration parts, and the remaining dimensions are assigned to the child view as the parent container. Pass in the child view through the measure() method to start the measurement of the child view.

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);
    }
}

3, Layout subview

The layoutDecoratedWithMargins() method is used for layout. You can see that the layout() method is called, and the layout of the child view is entered here.

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);
}

So far, we have sorted out how the sub view measures the layout. Returning to the fill() method, let's take a look at several criteria for ending the while loop:

1, remainingSpace is less than or equal to 0,

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

The initial value of remainingSpace is layoutstate.maavailable + layoutstate.mextrafillspace. Here, the value of maavailable is determined by updateLayoutStateToFillStart()/updateLayoutStateToFillEnd(). The specific code is reflected in onLayoutChildren(). There are several cases in which the specific method is determined. First, according to mLayoutFromEnd in the anchor information, usually the value we encounter is false, representing the layout from scratch. In this case, start with the anchor point, fill in the child controls behind the item corresponding to the anchor point, and call updateLayoutStateToFillEnd() to set various properties of mLayoutState, including mcavailable; Then fill in the child controls in front of the anchor point and call updateLayoutStateToFillStart() to set various properties of mLayoutState.

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();
    ...
}

mExtraFillSpace is related to sliding. In order to make it smoother when sliding, mExtraFillSpace will assign a value of msorientationhelper. Gettotalspace() to fill in an additional sub view of the page. In other cases, this value is 0.

After entering the while loop body, remainingSpace will subtract layoutChunkResult.mConsumed every time. layoutChunkResult.mConsumed is assigned in layoutChunk(), and the value is msorientationhelper.getdecoratedmeasurement (view).

2, layoutState.hasMore(state) is false,

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

The initial value of mCurrentPosition is the mPosition corresponding to the anchor point. Each time layoutState.next(recycler) gets the view, it will be + 1 / - 1 according to the filling direction.

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

3, After calling layoutChunk(), if layoutChunkResult.mFinished is true, it means that there are no child controls to be filled. At this time, the operation of jumping out of the while loop is executed.

setMeasuredDimension

As can be seen from the above, setMeasuredDimension is used to process the length and width dimensions of RecyclerView, with wrap in it_ Content. In this case, the measuredWidth/measuredHeight of RecyclerView is determined by the maximum measured length / width in the child controls.

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);
}

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); 
}

summary

This article combs the code related to LinearLayoutManager drawing. LayoutManager carries the drawing of child controls in RecyclerView (the content of this article), the recycling and reuse of child controls, and the related logic and optimization during sliding. Because there are too many things to carry, all the code is entangled, and I want to comb every line as clearly as possible, so it's very painful to write. It's not too long, but it takes a long time. I hope to clarify the drawing part of LLM.

The price of flexibility is complexity

Tags: Android

Posted on Thu, 25 Nov 2021 16:24:04 -0500 by phpshift