The problem background

In development, we often encounter the need for ScrollView nested RecyclerView, such as meituanmerchants home page this style:

Home page of Meituan takeout business


Ignoring the interaction of details, the home page of Meituan takeout merchants can be roughly abstracted into two parts:

  • The introduction to the business at the top (let’s call it the Header)
  • The list of items at the bottom (let’s call it Content)

For those unfamiliar with Android development specifications, the layout might look something like this:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:padding="15dp"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="@color/colorAccent"
            android:gravity="center"
            android:text="I am the introduction of the business, our family meal thief delicious, preferential also thief, buy is to earn."
            android:textColor="#fff"
            android:textSize="20dp" />
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/design_default_color_primary"/>
    </LinearLayout>
</ScrollView>
Copy the code

Problems arise

The above layout looks fine, let’s run it and see what it looks like:

What’s The Fuck!


Looking at the image above, I’m sure most of you would have been crushed: What the hell, why didn’t the Header slide along with the Content? You said ScrollView, why don’t you just roll?

Why doesn’t the Header scroll

Let’s look at the official ScrollView definition:

/** * A view group that allows the view hierarchy placed within it to be scrolled. * ScrollView may have only one direct child placed within it. * ScrollView may have only one direct child placed within it. Irrelevant comments are omitted here... * /Copy the code

As you can see from the comments, the most concise official positioning of a ScrollView is a layout that allows its internal layout to scroll. Let’s take a look at the RecyclerView positioning:

/**
 * A flexible view forProviding a limited window into a large data set. * A flexible view used to display a large amount of data in a limited window. * /Copy the code

To present a large amount of data in a limited window is, in plain English, to present a large amount of data in a limited space, in a rolling manner (the word “limited” is important here; we’ll use it later). Here’s the problem: Both views can scroll, and when we swipe our fingers across the screen, Android doesn’t know which view we want to scroll to, which is often referred to as a slide conflict.

In addition, ScrollView will call getView() method in Adapter to load all items of ListView into memory, consuming a lot of memory and CPU resources, resulting in interface jam. That is why in alibaba Android development handbook ScrollView nested ListView/GridView/ExpandableListView is prohibited.

Alibaba Android Development manual


Sliding conflict resolution

There are two main solutions to solve sliding conflict. One is based on the traditional event distribution mechanism; Use NestedScrollingChild & NestedScrollingParent. There are many online tutorials on the first option, so I won’t go into them here. Scrolling for NestedScrollingChild and NestedScrollingParent

Due to the shortcomings of the traditional event distribution mechanism (the parent layout intercepts the consumption slide event and cannot continue to pass it to the child View), we recommend the second way to resolve the slide conflict.

Of course, we don’t need to start with NestedScrollingParent and NestedScrollingChild just to solve the problem here, because many of Android’s built-in controls already implement both interfaces. And that includes the NestedScrollView that we’re going to talk about. As a high-frequency control in daily development, RecyclerView also realizes this mechanism, of course.

Let’s put on NestedScrollView, okay?

For NestedScrollView, the official definition looks like this:

/** * NestedScrollView is just like {@link android.widget.ScrollView}, NestedScrollView is similar to ScrollView, but it supports acting as both a nested scrolling parent and child on both new and old Versions of Android. * However, it supports acting as both a superview and a subview for nested scrolling on both old and new versions of Android. * Nested scrolling is enabled by default. * /Copy the code

NestedScrollView seems to be the perfect solution, so let’s try NestedScrollView instead of the root layout:

<android.support.v4.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout                   
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:padding="15dp"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="@color/colorAccent"
            android:gravity="center"
            android:text="I am the introduction of the business, our family meal thief delicious, preferential also thief, buy is to earn."
            android:textColor="#fff"
            android:textSize="20dp" />
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/design_default_color_primary"/>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>
Copy the code

Here’s how it works:

With the NestedScrollView, the interaction looks exactly as we expected


Clap your hands 👏 scatter flowers 🎉, it looks like we have solved the problem successfully. Is that true?

New problems have arisen

As we all know, RecyclerView is needed to combine Adapter to use, Adapter has several key methods:

  • onViewAttachedToWindow
  • onCreateViewHolder
  • onBindViewHolder … OnViewAttachedToWindow (); onViewAttachedToWindow ();
/** * Called when a view created by this adapter has been attached to a window. * Called when the view created by Adapter via the onCreateViewHolder method is attached to the window. * /Copy the code

That is, it’s called when the views in RecyclerView scroll to the screen where we can see them. RecyclerView onViewAttachedToWindow(); RecyclerView onViewAttachedToWindow(); RecyclerView onViewAttachedToWindow();

override fun onViewAttachedToWindow(holder: ViewHolder) {
        super.onViewAttachedToWindow(holder)
        Log.e(TAG, "onViewAttachedToWindow:" +holder.tvPosition.text.toString())
    }
override fun getItemCount(): Int {
        return50}Copy the code

The call is as follows:

The 2019-09-06 17:59:02. 161, 24351-24351 / com. Vision. Advancedui E/MyAdapter: OnViewAttachedToWindow: Position: 0 2019-09-06 17:59:02. 165, 24351-24351 / com. Vision. Advancedui E/MyAdapter: OnViewAttachedToWindow: Position: 1 2019-09-06 17:59:02. 168, 24351-24351 / com. Vision. Advancedui E/MyAdapter: OnViewAttachedToWindow: Position: 2 2019-09-06 17:59:02. 171, 24351-24351 / com. Vision. Advancedui E/MyAdapter: OnViewAttachedToWindow: Position: 3... Omit 45 similar ones herelog. The 2019-09-06 17:59:02. 304, 24351-24351 / com. Vision. Advancedui E/MyAdapter: onViewAttachedToWindow: Position: 49Copy the code

Through the log, we can clearly see, RecyclerView almost instantaneously loaded all (here is 50) items, and Google official description of “on demand loading” is completely different, is Google annotated description wrong?

NestedScrollView is recommended in Alibaba Android Development Manual


Examples of such usage are also included in the Alibaba Android Development Specification, and are marked for “correct” usage. What went wrong?

Get to the root of the problem

As mentioned above, Google’s position on RecyclerView is: in a limited window to show a large amount of data, it is easy for us to think that the height of RecyclerView measurement is wrong?

View drawing process

I believe most of you know the general Android drawing process (put the elephant in the refrigerator, total steps? :

  1. measure
  2. layout
  3. draw

For the View inherited from the ViewGroup, in addition to determine its own size, but also to help the child View measurement, determine their size, this, ViewGroup provides a static method getChildMeasureSpec:

/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
Copy the code

The MeasureSpec method returns a MeasureSpec, about MeasureSpce, which translates into Chinese as the measure specification. It is a 32-bit int type, with the high 2 bits representing the measure mode and the low 30 bits representing the measure size. There is a lot of information about it on the Internet, but I won’t go into it here. As long as we know here, there are three measurement modes:

  • The parent layout does not limit the size of the child layout. It has no restrictions.
  • The parent layout determines the final size of the child layout.
  • The parent layout does not determine the final size of the child layout, but the size of the child layout cannot exceed the size given by the parent layout.

This is the table (borrowed from Ren Yugang’s picture) :

Where is the MeasureSpec parameter child View that this method returns? The ViewGroup will call measureChild and pass getChildMeasureSpec to the measure method of the child View. The measure method will continue to call the onMeasure(int widthMeasureSpec, int heightMeasureSpec) method we often use for custom views, The widthMeasureSpec and heightMeasureSpec parameters are passed from the parent layout. Let’s look at the onMeasure method in the View class:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
Copy the code

We can assume that calling the setMeasureDimension method marks the completion of the measurement for the child View and that its height and width are determined accordingly. The final measurement is achieved by repeating the process recursively.

Has the RecyclerView measurement gone wrong?

Going back to our question, through a review of the above View measurement process, we can determine: The height of RecyclerView is determined by the MeasureSpec parameter and onMeasure in the RecyclerView passed to the RecyclerView in NestedScrollView The MeasureSpec parameter is passed to RecyclerView in NestedScrollView, and the measureChild method in NestedScrollView is written as: MeasureSpec

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();

        int childWidthMeasureSpec;
        int childHeightMeasureSpec;

        childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
                + getPaddingRight(), lp.width);

        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
Copy the code

As we can see, the measurement mode transmitted to RecyclerView with respect to the height is UNSPECIFIED. Next look at onMeasure() in RecyclerView:

protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || 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. / / this line of code is key 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); }}else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            // custom onMeasure
            if (mAdapterUpdateDuringMeasure) {
                eatRequestLayout();
                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;
                resumeRequestLayout(false);
            }

            if(mAdapter ! = null) { mState.mItemCount = mAdapter.getItemCount(); }else {
                mState.mItemCount = 0;
            }
            eatRequestLayout();
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            resumeRequestLayout(false);
            mState.mInPreLayout = false; // clear
        }
    }
Copy the code

The RecyclerView has no precise width and height + at least one child also has an imprecise width and height when it needs to be measured twice, the height measurement mode is EXACTLY, and the RecyclerView has no precise width and height + at least one child also has an imprecise width and height when it needs to be measured twice. The rest are called mLayout. SetMeasuredDimensionFromChildren (widthSpec heightSpec) to determine the size of the RecyclerView.

When the mAutoMeasure property is true, the comment in the source code says:

       /**
         * Defines whether the layout should be measured by the RecyclerView or the LayoutManager
         * wants to handle the layout measurements itself.
         * <p>
         * This method is usually called by the LayoutManager with value {@code true} if it wants
         * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize
         * the measurement logic, you can call this method with {@code false} and override
         * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic.
         * <p>
         * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or
         * handle various specs provided by the RecyclerView's parent. * It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an * {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based * on children's positions. It does this while supporting all existing animation
         * capabilities of the RecyclerView.
Copy the code

** If the RecyclerView LayoutManager supports the WRAP_CONTENT attribute, this value should be true. Now I’m sure you’re wondering: Which LayoutManagers support the WRAP_CONTENT attribute? Here’s what the source comment says:

       /**
         * AutoMeasure works as follows:
         * <ol>
         * <li>LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of
         * the framework LayoutManagers use {@code auto-measure}.</li>
         */
Copy the code

This means that the mAutoMeasure property of the native LayoutManager provided by Android is true.

We’ll look at setMeasuredDimensionFromChildren method.

        void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
            final int count = getChildCount();
            if (count == 0) {
                mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
                return;
            }
            int minX = Integer.MAX_VALUE;
            int minY = Integer.MAX_VALUE;
            int maxX = Integer.MIN_VALUE;
            int maxY = Integer.MIN_VALUE;

            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; } // Recycle all subviews, RecyclerView To the left, top, right, bottom four values assigned to mTempRect mRecyclerView. MTempRect. Set (minX, minY, maxX, maxY); // Really determine the RecyclerView height codesetMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
        }
Copy the code

Setmeasuredimension: SetMeasureDimension: setMeasureDimension: SetMeasureDimension

public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight(); // Child View height: padding + height int usedHeight = childrenbound.height () + getPaddingTop() + getPaddingBottom(); int width = chooseSize(wSpec, usedWidth, getMinimumWidth()); Int height = chooseSize(hSpec, usedHeight, getMinimumHeight()); // Calling this method marks the end of the measurementsetMeasuredDimension(width, height);
}
Copy the code

Finally, we are positioned to: the RecyclerView height of the determination of the focus depends on the chooseSize method, let’s look at:

        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));
                caseThe MeasureSpec. UNSPECIFIED: default: / / desired is heresetThe height of the child View in MeasuredDimensionreturnMath.max(desired, min); }}Copy the code

MeasureSpec: RecyclerView as the measure mode UNSPECIFIED: RecyclerView returns the maximum value between the height of the RecyclerView neutron View and the minimum value. MeasureSpec: RecyclerView as the Measure mode UNSPECIFIED: RecyclerView returns the maximum value between the height of the RecyclerView neutron View and the minimum value of the RecyclerView. MeasureSpec: RecyclerView as the measure mode UNSPECIFIED: RecyclerView as the RecyclerView neutron View.

This is what we have described above: the layout has an UNSPECIFIED size, which means it can be as large as you want. The final RecyclerView height is the height of all subviews.

Problem found

Through the above exploration, I believe that you should be very clear about the cause of the problem: The measurement mode of the NestedScrollView transmitted to the subview is UNSPECIFIED, and the RecyclerView, in the measurement mode of UNSPECIFIED, does not limit its height. The RecyclerView window height will become the sum of all item heights plus paddding height. Therefore, it appears that the item is loaded all at once. This may not be a problem when the number of RecyclerView items is small, but if the number of items is large, the resulting performance problems will be very serious.

So here I venture to sound different: prohibit NestedScrollView nested RecyclerView.

How does this layout work

It is recommended to use the layout of RecyclerView, after all, RecyclerView comes with sliding, there is no need to cover a ScrollerView or NestedScrollView. Or use CoordinatorLayout to do more

Write in the last

Before this article came out, my heart is also full of uneasy, after all, when I began to contact Android, I also think “Alibaba Android Development Manual” can not be wrong. I didn’t expect the response of the article would be so big, so here is a unified reply to the more topics mentioned in the comments:

Q: RecyclerView height does not use WRAP_CONTENT but use a specific value (e.g. 200DP) is not this problem? A: The answer is yes. We can also know from the table summarized by Ren Yugang: The measure mode of the child View is UNSPECIFIED, or the height in the layoutParams of the child View is set to WRAP_CONTENT or MATCH_PARENT, and the measure mode of the child View is UNSPECIFIED.

Q: Alibaba Android Development Manual does not advocate that we use NestedScrollView nested RecyclerView, But to advocate that we use NestedScrollView nested RecyclerView scheme to replace ScrollView nested RecyclerView scheme. A: In this case, NestedScrollView nesting RecyclerView does have problems. In addition to the performance impact, if the project has other operations (such as exposure) in the onAttachViewToWindow, it will affect the accuracy of the operation. This “Alibaba Android Development Manual” does not mention, the original intention of this article is to let everyone on NestedScrollView nested RecyclerView shortcomings have a specific cognition, and, I personally do not agree with the use of NestedScrollView nested RecyclerView regardless of circumstances.

Q: When the data amount of RecyclerView is small, can use NestedScrollView nested RecyclerView? A: When the number of RecyclerViews is manageable, using NestedScrollView to nest recyclerViews may indeed not have a performance problem, and if there is no extension to the onAttachViewToWindow method in the Adapter, there is indeed no other impact. But personally I don’t recommend this: RecyclerView itself supports sliding, there is no need to nest NestedScrollView in the outer layer, NestedScrollView nested NestedScrollView program in addition to the development of the time to save some time other no benefits. Of course, writing this article does not require everyone to follow such a way to achieve, after all, others say good, may not be suitable for your project.

Finally, people always think “alibaba Android development manual” is a good handbook, the above offers many Android developers really notice, the individual also has benefited greatly from, this article is aimed at the point to talk about some of their different understanding, after all, open source platform “schools of thought contend”. Finally, thank you for liking my article, I really appreciate it.