An overview of the

RecyclerView is so familiar and so strange. Familiar because as an Android developer, RecyclerView is often used in the project, strange because JUST know how to use, but do not know the internal realization mechanism of RecyclerView.

But every developer who has something to pursue will not just let himself stay in use, but will read the relevant source code, know why.

For RecyclerView source code analysis of an article is certainly not enough, there will be time to continue to update. Each will have its own theme. The use of RecyclerView, this article will not talk about, specific can see before the article: RecyclerView use guide.

For developers who have used RecyclerView, the function of this View is really powerful, can be used in many scenarios of daily development. When explaining RecyclerView draw source code, I hope you to think about some problems:

  1. If you, how would you design RecyclerView drawing process, and ordinary View?
  2. RecyclerView can support different flow layouts, one column, multiple columns, so how is the drawing logic inside it designed?
  3. The splitter line is customizable, so how do I design this piece of code?

There are other issues as well, but this article will only focus on the drawing process, so the other issues will be addressed in other modules. In the old days, I analyzed the source code for the sake of analyzing it and then sent it out. Very little thought is given to the stuff behind the source code. Until recently, when I needed to refactor a module, I realized how difficult it was to design a technical solution.

In this paper, the source code version: androidx. RecyclerView: 1.1.0

The measure to measure

For view, there must be three processes: measurement, layout, drawing. So RecyclerView is the same thing. If you are not familiar with the drawing process of a View, you can refer to the article:

  • Android View drawing process Measure process details (1)
  • Android View drawing process Layout and Draw process details (2)

Now enter the topic, first look at the definition of RecyclerView class:

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
     // ......  
}
Copy the code

It can be seen that RecyclerView is a ViewGroup, that is to say, RecyclerView is essentially a custom view, which needs to manage the drawing process by itself. To understand custom views, you need to override the onMeasure method.

Android custom View in detail in this article summarizes the specific logic of onMeausre, here, can still do a reference:

  1. Super. onMeasure calculates the size of the custom view first.
  2. Call measureChild to measureChild views;
  3. Custom view width and height is not EXACTLY MeasureSpec.EXACTLY if the child is match_parent, extra processing is required for MeasureSpec.AT_MOST.
  4. When the size of the custom View is determined, the child View is match_parent.

Here is the RecyclerView onMeausre code:

protected void onMeasure(int widthSpec, Int heightSpec) {if (mLayout = = null) {/ / in the first case} the if (mLayout. IsAutoMeasureEnabled ()) {/ / the second} else {/ / the third situation}}Copy the code

The onMeasure method is still a bit long, so I’ve divided it into three cases, which I’ll briefly explain:

  1. mLayout namelyLayoutManager The object. We know whenRecyclerView theLayoutManager Is empty,RecyclerView Can’t display any data, here we find the answer.
  2. LayoutManager This is the case when you have automatic measurement turned on. In this case, it is possible to measure twice.
  3. The third case is the case where automatic measurement is not turned on, which is relatively rare, becauseRecyclerView tosupportwarp_content Property, provided by the systemLayoutManager They’re all on automatic measurement, but we still need to analyze it.

So let’s do the first case.

LayoutManager == null

In this case, it’s easier. Let’s look at the source code:

        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
Copy the code

Here we call the defaultOnMeasure method,

    void defaultOnMeasure(int widthSpec, int heightSpec) {
        // calling LayoutManager here is not pretty but that API is already public and it is better
        // than creating another method since this is internal.
        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);
    }
Copy the code

In the defaultOnMeasure method, the width is calculated using LayoutManager’s chooseSize method, and then setMeasuredDimension is called to set the width. Here’s chooseSize’s logic:

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: return Math.max(desired, min); }}Copy the code

 

The main thing here is to return the final size, depending on the Settings. Readers who are not familiar with this logic can read the previous article, which explains it in detail. But one thing to point out here is that we didn’t measure the size of the subview, and that’s why we got a blank screen. Because RecyclerView is actually entrusted to the LayoutManager to manage, LayoutManager = null to measure the child view does not make any sense.

LayoutManager enables automatic measurement

Before we analyze this situation, let’s know a few things.

The measurement of RecyclerView is divided into two steps, calling dispatchLayoutStep1 and dispatchLayoutStep2 respectively. At the same time, students who understand RecyclerView source code should know that there is also a dispatchLayoutStep3 method in RecyclerView source code. These three methods have similar method names, so it can be confusing. This article will explain the functions of these three methods in detail.

Since only dispatchLayoutStep1 and dispatchLayoutStep2 are called in this case, we will focus on these two methods here. And dispatchLayoutStep3 method call in RecyclerView onLayout method inside, so in the later analysis of onLayout method to see dispatchLayoutStep3 method.

Before we do that, let’s look at something called mstate.mlayoutstep. There are several possible values for this variable. Let’s take a look at each:

The values meaning
State.STEP_START mState.mLayoutStep In this case, RecyclerView is not yet experienceddispatchLayoutStep1Because thedispatchLayoutStep1 After the callmState.mLayoutStep Will becomeState.STEP_LAYOUT.
State.STEP_LAYOUT whenmState.mLayoutStep forState.STEP_LAYOUT Is in the Layout phase, which is calleddispatchLayoutStep2 methodslayout RecyclerView thechildren. calldispatchLayoutStep2 Method, at this pointmState.mLayoutStep Into theState.STEP_ANIMATIONS.
State.STEP_ANIMATIONS whenmState.mLayoutStepforState.STEP_ANIMATIONS Said,RecyclerView We are in the third stage, where we execute the animation, which is calleddispatchLayoutStep3Methods. whendispatchLayoutStep3 After the method is executed,mState.mLayoutStep And into theState.STEP_START.

From the table above, we know that the three states of mstate.mlayoutstep correspond to different dispatchLayoutStep methods. We need to be clear about this, or the rest of the code will be hard to understand.

if (mLayout.isAutoMeasureEnabled()) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); /** * This specific call should be considered deprecated and replaced with * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could * break existing third party code but all documentation directs developers to not * override {@link LayoutManager#onMeasure(int, int)} when * {@link LayoutManager#isAutoMeasureEnabled()} returns true. */ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) { return; } if (mstate.mlayoutstep == state.step_start) {dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency mLayout.setMeasureSpecs(widthSpec, heightSpec); MState. MIsMeasuring = true; // dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); // if RecyclerView has non-exact width and height and if there is at least one child // which also has non-exact width &  height, we have to re-measure. 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); }}Copy the code

First, let’s look at the onMeasure method.

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

Found that the call of RecyclerView defaultOnMeasure method, in fact, is the three steps of the custom View we introduced before: first measure their own size.

dispatchLayoutStep1

Let’s go further and look at the logic of the dispatchLayoutStep1() method:

/** * 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() mState.assertLayoutStep(State.STEP_START); fillRemainingScrollValues(mState); mState.mIsMeasuring = false; startInterceptRequestLayout(); mViewInfoStore.clear(); onEnterLayoutOrScroll(); / / processing adapter updated processAdapterUpdatesAndSetAnimationFlags (); saveFocusInfo(); mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; mItemsAddedOrRemoved = mItemsChanged = false; mState.mInPreLayout = mState.mRunPredictiveAnimations; mState.mItemCount = mAdapter.getItemCount(); FindMinMaxChildLayoutPositions (mMinMaxLayoutPositions); / / whether to run the animation the if (mState. MRunSimpleAnimations) {/ / Step 0: Find out where all non-removed items are, pre-layout int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore() || (holder.isInvalid() && ! mAdapter.hasStableIds())) { continue; } final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo); if (mState.mTrackOldChangeHolders && holder.isUpdated() && ! holder.isRemoved() && ! holder.shouldIgnore() && ! holder.isInvalid()) { long key = getChangedHolderKey(holder); // This is NOT the only place where a ViewHolder is added to old change holders // list. There is another case where: // * A VH is currently hidden but not deleted // * The hidden item is changed in the adapter // * Layout manager decides  to layout the item in the pre-Layout pass (step1) // When this case is detected, RV will un-hide that view and add to the old // change holders list. mViewInfoStore.addToOldChangeHolders(key, holder); } } } if (mState.mRunPredictiveAnimations) { // Step 1: run prelayout: This will use the old positions of items. The layout manager // is expected to layout everything, even removed items (though not to add removed // items back to the container). This gives the pre-layout position of APPEARING views // which come into existence as part of the real layout. // Save old positions so that LayoutManager can  run its mapping logic. saveOldPositions(); final boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged = false; // temporarily disable flag because we are asking for previous layout mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = didStructureChange; for (int i = 0; i < mChildHelper.getChildCount(); ++i) { final View child = mChildHelper.getChildAt(i); final ViewHolder viewHolder = getChildViewHolderInt(child); if (viewHolder.shouldIgnore()) { continue; } if (! mViewInfoStore.isInPreLayout(viewHolder)) { int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder); boolean wasHidden = viewHolder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (! wasHidden) { flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; } final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads()); if (wasHidden) { recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo); } else { mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); } } } // we don't process disappearing list because they may re-appear in post layout pass. clearOldPositions(); } else { clearOldPositions(); } onExitLayoutOrScroll(); stopInterceptRequestLayout(false); MLayoutStep = state.step_layout; // Change the State mstate.mlayoutStep = state.step_layout; }Copy the code

The top method comment summarizes what this method does. The main work of this method is as follows:

  1. To deal withAdapter Update;
  2. Decide which animation to perform
  3. Save eachItemView The information of
  4. Pre-layout, if necessary, and save relevant information.

As you can see, there are still a lot of methods called inside the whole method, causing you to feel that the logic of this method is very complicated. But now that is the source code to read, let’s focus on some important points, in the most by the method called processAdapterUpdatesAndSetAnimationFlags is need to go in and see the inside of the logical, later if the else logic are determined in the method.

/** * Consumes adapter updates and calculates which type of animations we want to run. * Called in onMeasure and dispatchLayout. * <p> * This method may process only the pre-layout state of updates or all of them. */ private void processAdapterUpdatesAndSetAnimationFlags() { 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); } } // simple animations are a subset of advanced animations (which will cause a // pre-layout step) // If layout supports predictive animations, pre-process to decide if we want to run them if (predictiveItemAnimationsEnabled()) { mAdapterHelper.preProcess(); } else { mAdapterHelper.consumeUpdatesInOnePass(); } boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator ! = null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (! mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && ! mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); }Copy the code

Here we are focusing on the mFirstLayoutComplete variable. We can see that mRunSimpleAnimations have a value associated with mFirstLayoutComplete, MRunPredictiveAnimations are also related to mRunSimpleAnimations. So here we can conclude that when the RecyclerView first loads the data, it’s not going to execute the animation, right? Will that be true or not? I’ll keep you in suspense.

dispatchLayoutStep2

Now let’s look at the dispatchLayoutStep2 method, which is really laying out the children. The code:

/** * The second layout step where we do the actual layout of the views for the final state. * This step might be run multiple times if necessary (e.g. measure). */ private void dispatchLayoutStep2() { startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); mState.mItemCount = mAdapter.getItemCount(); mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; // Step 2: Run layout mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mPendingSavedState = null; // onLayoutChildren may have caused client code to disable item animations; re-check mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator ! = null; mState.mLayoutStep = State.STEP_ANIMATIONS; onExitLayoutOrScroll(); stopInterceptRequestLayout(false); }Copy the code

As you can see, the logic here seems a lot simpler, and that’s because I’m putting the subview drawing logic into the LayoutManager. Here, the state of the state has changed, became a state. The STEP_LAYOUT | state. STEP_ANIMATIONS.

The onLayoutChildren method of the system’s LayoutManager is an empty method, so a subclass of LayoutManager needs to implement it itself.

Without going into too much detail here, different LayoutManagers have different implementations.

3. Automatic measurement is not enabled

Let’s first look at the code for this section:

{ if (mHasFixedSize) { mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); return; } // custom onMeasure if (mAdapterUpdateDuringMeasure) { startInterceptRequestLayout(); onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); onExitLayoutOrScroll(); if (mState.mRunPredictiveAnimations) { mState.mInPreLayout = true; } else { // consume remaining updates to provide a consistent state with the layout pass. mAdapterHelper.consumeUpdatesInOnePass(); mState.mInPreLayout = false; } mAdapterUpdateDuringMeasure = false; stopInterceptRequestLayout(false); } else if (mState.mRunPredictiveAnimations) { // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is  true: // this means there is already an onMeasure() call performed to handle the pending // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time // because getViewForPosition() will crash when LM uses a child to measure. setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()); return; } if (mAdapter ! = null) { mState.mItemCount = mAdapter.getItemCount(); } else { mState.mItemCount = 0; } startInterceptRequestLayout(); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); stopInterceptRequestLayout(false); mState.mInPreLayout = false; // clear }Copy the code

There are two main things done here, which are actually very similar to the second step, and they will eventually call the onMeasure method of LayoutManager to measure.

  • ifmHasFixedSizeIs true(that is, calledsetHasFixedSizeMethod), will be called directlyLayoutManagertheonMeasureMethods To measure.
  • ifmHasFixedSizeIs false, and if there is data update, the transaction for data update is processed first and then calledLayoutManagertheonMeasureMethods to measure

OnLayout layout

Here, about the measurement logic is finished, then start to look at the layout logic:

    @Override
    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

The dispatchLayout operation is called first, and you can see that one of the variables we looked at earlier, mFirstLayoutComplete, is assigned to true.

Here is the dispatchLayout method:

void dispatchLayout() { if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); // leave the state in START return; } if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START 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

DispatchLayout method is also very simple, this method to ensure that RecyclerView must go through three processes — dispatchLayoutStep1, dispatchLayoutStep2, dispatchLayoutStep3. At the same time, if it is found that the sub-view width and height parameters change, the dispatchLayoutStep2() method will be called again.

Finally, let’s look at the dispatchLayoutStep3 method:

/** * The final step of the layout where we save the information about views for animations, Trigger animations and do any necessary cleanup. */ private void dispatchLayoutStep3() {// animations mState.assertLayoutStep(State.STEP_ANIMATIONS); startInterceptRequestLayout(); onEnterLayoutOrScroll(); // mark the reset mstate.mlayoutStep = state.step_start; if (mState.mRunSimpleAnimations) { // Step 3: Find out where things are now, and process change animations. // traverse list in reverse because we may call animateChange in the loop which may // remove the target view holder. for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore()) { continue; } long key = getChangedHolderKey(holder); final ItemHolderInfo animationInfo = mItemAnimator .recordPostLayoutInformation(mState, holder); ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); if (oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { // run a change animation // If an Item is CHANGED but the updated version is disappearing, it creates // a conflicting case. // Since a view that is marked as disappearing is likely to be going out of // bounds,  we run a change animation. Both views will be cleaned automatically // once their animations finish. // On the other hand, if it is the same view holder instance, we run a // disappearing animation instead because we are not going to rebind the updated // VH unless it is enforced by  the layout manager. final boolean oldDisappearing = mViewInfoStore.isDisappearing( oldChangeViewHolder); final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); if (oldDisappearing && oldChangeViewHolder == holder) { // run disappear animation instead of change mViewInfoStore.addToPostLayout(holder, animationInfo); } else { final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( oldChangeViewHolder); // we add and remove so that any post info is merged. mViewInfoStore.addToPostLayout(holder, animationInfo); ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); if (preInfo == null) { handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); } else { animateChange(oldChangeViewHolder, holder, preInfo, postInfo, oldDisappearing, newDisappearing); } } } else { mViewInfoStore.addToPostLayout(holder, animationInfo); }} // Step 4: Process view info Lists and trigger animations MViewinfostore. Process (mViewInfoProcessCallback); } mLayout. RemoveAndRecycleScrapInt (mRecycler); / / record data and use symbols before the reset mState. MPreviousLayoutItemCount = mState. MItemCount; mDataSetHasChangedAfterLayout = false; mDispatchItemsChangedEvent = false; mState.mRunSimpleAnimations = false; mState.mRunPredictiveAnimations = false; mLayout.mRequestedSimpleAnimations = false; if (mRecycler.mChangedScrap ! = null) { mRecycler.mChangedScrap.clear(); } if (mLayout.mPrefetchMaxObservedInInitialPrefetch) { // Initial prefetch has expanded cache, so reset until next prefetch. // This prevents initial prefetches from expanding the cache permanently. mLayout.mPrefetchMaxCountObserved = 0; mLayout.mPrefetchMaxObservedInInitialPrefetch = false; mRecycler.updateViewCacheSize(); } mLayout.onLayoutCompleted(mState); onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mViewInfoStore.clear(); // if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { dispatchOnScrolled(0, 0); } recoverFocusFromState(); resetFocusInfo(); }Copy the code

It can be seen from the above logic that dispatchLayoutStep3 is mainly to animate Item. This paper does not expand the animation, so it first omits the animation part. Then reset some flag bits. Clear some states.

summary

Here is a summary of these three methods to help you remember the functions of these methods:

The method name role
dispatchLayoutStep1 There are three main functions of this method: 1Adapter Update;
2. Decide which animation to perform
3. Save eachItemView The information of
4. Pre-layout, if necessary, and save relevant information.
dispatchLayoutStep2 In this method, it’s really going onchildren Measurement and layout.
dispatchLayoutStep3 The effect of this method is implemented indispatchLayoutStep1 Method to save the animation information. This method is not the focus of this article


\

3, Draw

Next, let’s analyze the final stage of the three processes, CALLED DRAW.

RecyclerView’s draw() and onDraw() methods

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); } / /... }Copy the code

That’s very thoughtful.

@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

I’m not doing much here, I’m just calling the onDraw and onDrawOver methods of ItemDecoration. This adds the divider to it.

4、 LinearLayoutManager

Before the introduction of dispatchLayoutStep2 method, just a simple introduction, RecyclerView by calling LayoutManager onLayoutChildren method. The LayoutManager itself doesn’t implement this method, so you’ll have to look at its subclasses, which use LinearLayoutManager as an example:

onLayoutChildren

@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ...... ensureLayoutState(); mLayoutState.mRecycle = false; / / resolve layout direction resolveShouldLayoutReverse (); / /...Copy the code
/ / calculate the anchor position and coordinate updateAnchorInfoForLayout (recycler, state, mAnchorInfo); MAnchorInfo. MValid = true; / /...Copy the code
detachAndScrapAttachedViews(recycler); // This method is explained in the cache article. / /...Copy the code
// noRecycleSpace not needed: recycling doesn't happen in below's fill
        // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
        mLayoutState.mNoRecycleSpace = 0;
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } 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;
            }
        }

       
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
        if (!state.isPreLayout()) {
            mOrientationHelper.onLayoutComplete();
        } else {
            mAnchorInfo.reset();
        }
        mLastStackFromEnd = mStackFromEnd;
        if (DEBUG) {
            validateChildOrder();
        }
    }
Copy the code

The onLayoutChildren method is long, so you omit some extraneous code. In fact, it is mainly to do two things to determine the information of the anchor point, which includes:

  1. Children The layout direction of the start and end direction;
  2. mPosition andmCoordinate, respectively representChildren Start filling position and coordinates.

Based on the anchor information, the Children are populated by calling the fill method. Depending on the anchor information, the fill method may be called twice.

updateAnchorInfoForLayout

If you want to see the anchor the calculation process of information, we can from inside updateAnchorInfoForLayout method to find out, let’s take a look at updateAnchorInfoForLayout method:

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from pending information");
            }
            return;
        }

        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");
        }
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
Copy the code

I believe that through the above code comments, everyone can understand what did updateAnchorInfoForLayout method, here I do simple analysis about the three determine meaning, specific do, will not discuss here.

  1. The first method of calculation has two meanings: 1.RecyclerView It was rebuilt. It was called backonSaveInstanceState Method, so the purpose is to restore the last layout; 2.RecyclerView Call thescrollToPosition Or something like that, so the goal is to makeRecyclerView Roll to the right position. Therefore, the anchor information is calculated according to the above two cases.
  2. The second way to calculate it is fromChildren Above to calculate the anchor point information. This calculation method also has two cases: 1. If the current has a focusChild, then have the position of the currently focused Child to calculate the anchor point; 2. If no child has the focus, then the layout direction is set bymLayoutFromEnd To determine) get the first visible oneItemView Or the last oneItemView.
  3. If the first two methods fail, the third method is used, which is the default method.

Fill the layout

We then call the fill method to populate the Children. For a formal analysis of the filling process, let’s start with an image: \

 

The figure above shows three cases of fill. Where we can see the third case, where the fill method is called twice.

Let’s look at the fill method:

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // ······
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // ······
            layoutChunk(recycler, state, layoutState, layoutChunkResult);

        }
         // ······
    }
Copy the code

The fill method is long, so it omits a lot of extraneous code. First, you can see that there is a while method, which is used to add child views to RecyclerView until there is no more space. The padding is done by adding the Child based on the remaining space, and the padding is done by the layoutChunk method.

layoutChunk

Let’s look at the layoutChunk method.

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); . if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); }} / /... measureChildWithMargins(view, 0, 0); // Add a subview and measure it at the same time. // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout position, we subtract margins. layoutDecorated(view, left + params.leftMargin, top + params.topMargin, right - params.rightMargin, bottom - params.bottomMargin); . }Copy the code

Remind not to underestimate this next method, RecyclerView cache mechanism is the starting point from this method, you can imagine how much things this method does for us.

So the addView() method here, that’s the addView() method of the ViewGroup; The measureChildWithMargins() method is used to measure the size of child controls, as the name shows. If you’re not familiar with this method, check out my drawing of Views article at the beginning. For now, it is simply understood as measuring the size of child controls. Here is the layoutDecoreated() method:

public void layoutDecorated(@NonNull View child, int left, int top, int right, Final Rect insets = ((LayoutParams) child.getLayOutParams ()).mdecorinsets; child.layout(left + insets.left, top + insets.top, right - insets.right, bottom - insets.bottom); }Copy the code

To summarize the above code, in the measure and layout stage of RecyclerView, the algorithm of filling ItemView is as follows: Add child control to the parent container, measure child control size, layout child control, layout anchor point to the current layout direction translation child control size, repeat appeal steps to RecyclerView can draw space consumption or child control has been filled.

This completes the measure and layout process for all child controls. Back to RecyclerView onMeasure method, perform mLayout. SetMeasuredDimensionFromChildren (widthSpec heightSpec) the role of the line of code is according to the size of the child controls, Set the size of RecyclerView. At this point, measure and layout of RecyclerView has actually been completed.

However, you may have noticed the problem in the process above: how to determine RecyclerView drawable space? However, if you are familiar with the drawing mechanism of Android controls, this is not a problem. In fact, the drawable space here can be simply understood as the size of the parent container; A more accurate description of the size of the RecyclerView layout required by the parent can be obtained by the Measurespec.getSize () method.

conclusion

Here, the drawing process of RecyclerView is finished, because the main drawing process, no analysis of other, may lead to some jump in the whole logic, but does not hinder the understanding of the whole drawing process.

Finally, returning to the previous problem of the article, it can be found that RecyclerView actually delegates the drawing process to layoutManager, which is very different from ordinary custom view. Such flexible operation, can let the user to define a variety of styles, make RecyclerView use scene become more rich.

Secondly lies in the separation line processing, it does not treat the separation line as a sub-view, but in the layout of the sub-view, the separation line into consideration to leave a gap.

 

Refer to the article

RecyclerView source code analysis (a) – RecyclerView three processes

Android RecyclerView source code analysis