preface

Based on the 2021-01-05 revised edition, source code implementation ‘androidx. Recyclerview: recyclerview: 1.1.0’

Last article, said recycles reuse RecyclerView, this article, we talk about RecyclerView drawing process.

  • RecyclerView ItemDecoration (a)
  • RecyclerView Cache (2)
  • 【Android advanced 】RecyclerView drawing process (three)
  • RecyclerView group list add top effect (4)

onMeasure

Let’s take a look at RecyclerView#onMeasure()

    protected void onMeasure(int widthSpec, int heightSpec) {
        if (this.mLayout == null) {
            this.defaultOnMeasure(widthSpec, heightSpec);
        } else {
            if (!this.mLayout.isAutoMeasureEnabled()) {
                if (this.mHasFixedSize) {
                    this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
                    return;
                }

                if (this.mAdapterUpdateDuringMeasure) {
                    this.startInterceptRequestLayout();
                    this.onEnterLayoutOrScroll();
                    this.processAdapterUpdatesAndSetAnimationFlags();
                    this.onExitLayoutOrScroll();
                    if (this.mState.mRunPredictiveAnimations) {
                        this.mState.mInPreLayout = true;
                    } else {
                        this.mAdapterHelper.consumeUpdatesInOnePass();
                        this.mState.mInPreLayout = false;
                    }

                    this.mAdapterUpdateDuringMeasure = false;
                    this.stopInterceptRequestLayout(false);
                } else if (this.mState.mRunPredictiveAnimations) {
                    this.setMeasuredDimension(this.getMeasuredWidth(), this.getMeasuredHeight());
                    return;
                }

                if (this.mAdapter ! =null) {
                    this.mState.mItemCount = this.mAdapter.getItemCount();
                } else {
                    this.mState.mItemCount = 0;
                }

                this.startInterceptRequestLayout();
                this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
                this.stopInterceptRequestLayout(false);
                this.mState.mInPreLayout = false;
            } else {
                int widthMode = MeasureSpec.getMode(widthSpec);
                int heightMode = MeasureSpec.getMode(heightSpec);
                this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
                boolean measureSpecModeIsExactly = widthMode == 1073741824 && heightMode == 1073741824;
                if (measureSpecModeIsExactly || this.mAdapter == null) {
                    return;
                }

                if (this.mState.mLayoutStep == 1) {
                    this.dispatchLayoutStep1();
                }

                this.mLayout.setMeasureSpecs(widthSpec, heightSpec);
                this.mState.mIsMeasuring = true;
                this.dispatchLayoutStep2();
                this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
                if (this.mLayout.shouldMeasureTwice()) {
                    this.mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), 1073741824), MeasureSpec.makeMeasureSpec(this.getMeasuredHeight(), 1073741824));
                    this.mState.mIsMeasuring = true;
                    this.dispatchLayoutStep2();
                    this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); }}}}Copy the code

If we look at it from the top, first of all, mLayout is a LayoutManager, and if it’s null then defaultOnMeasure is executed

      void defaultOnMeasure(int widthSpec, int heightSpec) {
        int width = RecyclerView.LayoutManager.chooseSize(widthSpec, this.getPaddingLeft() + this.getPaddingRight(), ViewCompat.getMinimumWidth(this));
        int height = RecyclerView.LayoutManager.chooseSize(heightSpec, this.getPaddingTop() + this.getPaddingBottom(), ViewCompat.getMinimumHeight(this));
        this.setMeasuredDimension(width, height);
    }
Copy the code

As you can see, without measuring the height of the item, we call setMeasuredDimension to set the width and height

If isAutoMeasureEnabled is true or false, two sets of logic will be used. By checking the source code, it can be found that isAutoMeasureEnabled is mAutoMeasure in LayoutManager. The default value is false. But true in the LinearLayoutManager

IsAutoMeasureEnabled to true

LinearLayoutManager related code

    public boolean isAutoMeasureEnabled(a) {
        return true;
    }
Copy the code

The main logic of onMeasure is also when isAutoMeasureEnabled is true. Let’s move on

                int widthMode = MeasureSpec.getMode(widthSpec);
                int heightMode = MeasureSpec.getMode(heightSpec);
                this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
                boolean measureSpecModeIsExactly = widthMode == 1073741824 && heightMode == 1073741824;
                if (measureSpecModeIsExactly || this.mAdapter == null) {
                    return;
                }
Copy the code

If the width and height are absolute values, skip the onMeasure method.

                if (this.mState.mLayoutStep == 1) {
                    this.dispatchLayoutStep1();
                }
Copy the code

MLayoutStep default value is state.step_start is 1, about dispatchLayoutStep1 method, in fact, there is no need to analyze too much, because the analysis of the source code is mainly for the understanding of the drawing idea, if too much entanglements in the meaning of each line of code, then will be in great trouble. After that, this.mstate.mlayoutStep = 2; That is the STEP_LAYOUT state.

Next, dispatchLayoutStep2 is where the LayoutManager drawing is actually performed.

    private void dispatchLayoutStep2(a) {
        this.startInterceptRequestLayout();
        this.onEnterLayoutOrScroll();
        this.mState.assertLayoutStep(6);
        this.mAdapterHelper.consumeUpdatesInOnePass();
        this.mState.mItemCount = this.mAdapter.getItemCount();
        this.mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
        this.mState.mInPreLayout = false;
        this.mLayout.onLayoutChildren(this.mRecycler, this.mState);
        this.mState.mStructureChanged = false;
        this.mPendingSavedState = null;
        this.mState.mRunSimpleAnimations = this.mState.mRunSimpleAnimations && this.mItemAnimator ! =null;
        this.mState.mLayoutStep = 4;
        this.onExitLayoutOrScroll();
        this.stopInterceptRequestLayout(false);
    }
Copy the code

As you can see, RecyclerView gives the drawing of items to LayoutManager (mLayout.onLayoutChildren(this.mrecycler, this.mstate); More on LayoutManager in the next article.

After this, this.mstate.mLayOutStep = 4; That is the STEP_ANIMATIONS state.

IsAutoMeasureEnabled to false

	    if (mHasFixedSize) {
       		 // No need to remeasure
                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

First, there is no remeasurement when mHasFixedSize is true, which is set by the setHasFixedSize method

    /**
     * RecyclerView can perform several optimizations if it can know in advance that RecyclerView's
     * size is not affected by the adapter contents. RecyclerView can still change its size based
     * on other factors (e.g. its parent's size) but this size calculation cannot depend on the
     * size of its children or contents of its adapter (except the number of items in the adapter).
     * <p>
     * If your use of RecyclerView falls into this category, set this to {@code true}. It will allow
     * RecyclerView to avoid invalidating the whole layout when its adapter contents change.
     *
     * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
     */
    public void setHasFixedSize(boolean hasFixedSize) {
        mHasFixedSize = hasFixedSize;
    }
Copy the code

When the change of Item in Adapter does not affect the width and height of RecyclerView, you can set it to True to avoid recalculating the size of RecyclerView.

This is also one of the memory optimization methods of RecyclerView

When mAdapterUpdateDuringMeasure is true, you need to do to measure, and mAdapterUpdateDuringMeasure is set to true in triggerUpdateProcessor method

        void triggerUpdateProcessor(a) {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true; requestLayout(); }}Copy the code

The triggerUpdateProcessor method is called by the following method,

  • onItemRangeChanged
  • onItemRangeInserted
  • onItemRangeRemoved
  • onItemRangeMoved

RequestLayout () is not required according to the value of mHasFixedSize when the Adapter’s add, delete, change, insert methods are called.

As mentioned earlier, the main logic of onMeasure is true when isAutoMeasureEnabled, so why is the default value false in LayoutManager?

If isAutoMeasureEnabled is false, can item be drawn normally? Let’s try it out

We override the isAutoMeasureEnabled method to return false

    class MyLinLayoutManager extends LinearLayoutManager {
        public MyLinLayoutManager(Context context) {
            super(context);
        }

        @Override
        public boolean isAutoMeasureEnabled(a) {
            return false; }}Copy the code

Then set it to RecyclerView, and when it runs, it will find that the item can be displayed normally. Why? So this is the onLayout method

onLayout

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

Here it’s a little easier. Let’s look at the dispatchLayout method

    void dispatchLayout(a) {
        if (this.mAdapter == null) {
            Log.e("RecyclerView"."No adapter attached; skipping layout");
        } else if (this.mLayout == null) {
            Log.e("RecyclerView"."No layout manager attached; skipping layout");
        } else {
            this.mState.mIsMeasuring = false;
            if (this.mState.mLayoutStep == 1) {
                this.dispatchLayoutStep1();
                this.mLayout.setExactMeasureSpecsFrom(this);
                this.dispatchLayoutStep2();
            } else if (!this.mAdapterHelper.hasUpdates() && this.mLayout.getWidth() == this.getWidth() && this.mLayout.getHeight() == this.getHeight()) {
                this.mLayout.setExactMeasureSpecsFrom(this);
            } else {
                this.mLayout.setExactMeasureSpecsFrom(this);
                this.dispatchLayoutStep2();
            }

            this.dispatchLayoutStep3(); }}Copy the code

It can be seen that the main logic of onMeasure is re-executed here, which also explains why onMeasure directly skipped execution when we set a fixed width and height for RecyclerView before, but the sub-view can still be displayed.

Your recognition is the motivation for me to keep updating my blog. If it is useful, please give me a thumbs-up. Thank you