Resolving Measurement and Scrolling Conflicts When Nesting RecyclerView Inside ScrollView

Nesting a RecyclerView within a ScrollView frequently triggers rendering failures, including blank screens, truncated items, or erratic scroll behavior. This occurs because both components attempt to handle vertical scrolling and height measurement simultaneously, causing the parent ScrollView to constrain the RecyclerView improperly during the layout pass.

Common workarounds such as overrriding RecyclerView.onMeasure() with an expanded MeasureSpec, applying android:descendantFocusability="blocksDescendants", or implementing a naive custom LayoutManager that iterates through all items often result in incomplete rendering, excessive blank space, or IndexOutOfBoundsException during animations. A reliable approach requires a dedicated LayoutManager that accurate calculates child dimensions, respects item decorations, and properly negotiates measurement constraints with the parent scroll container.

The layout hierarchy should configure the ScrollView to expand its viewport and allow the RecyclerView to determine its own height based on content:

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/headerTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="Section Header" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/nestedRecycler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:nestedScrollingEnabled="false" />
    </LinearLayout>
</ScrollView>

Key attributes include android:fillViewport="true" on the ScrollView and android:nestedScrollingEnabled="false" on the RecyclerView to prevent scroll event interception conflicts.

The core resolution lies in a specialized LinearLayoutManager that overrides the measurement phase. Instead of relying on the default recycling behavior during layout, it measures each visible child, accumulates dimensions, and factors in padding, margins, and ItemDecoration offsets.

public class WrapContentLinearLayoutManager extends LinearLayoutManager {
    private static final int DIM_WIDTH = 0;
    private static final int DIM_HEIGHT = 1;
    private final int[] childBounds = new int[2];
    private final Rect decorationRect = new Rect();
    private final RecyclerView hostView;

    public WrapContentLinearLayoutManager(RecyclerView recyclerView) {
        super(recyclerView.getContext());
        this.hostView = recyclerView;
    }

    public WrapContentLinearLayoutManager(RecyclerView recyclerView, int orientation, boolean reverseLayout) {
        super(recyclerView.getContext(), orientation, reverseLayout);
        this.hostView = recyclerView;
    }

    @Override
    public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
        int widthMode = View.MeasureSpec.getMode(widthSpec);
        int heightMode = View.MeasureSpec.getMode(heightSpec);
        int widthSize = View.MeasureSpec.getSize(widthSpec);
        int heightSize = View.MeasureSpec.getSize(heightSpec);

        boolean isVertical = getOrientation() == VERTICAL;
        int totalWidth = 0;
        int totalHeight = 0;

        recycler.clear();

        int itemCount = getItemCount();
        for (int i = 0; i < itemCount; i++) {
            try {
                View child = recycler.getViewForPosition(i);
                measureChildWithDecorationsAndMargin(child, widthSpec, heightSpec, isVertical);
                
                if (isVertical) {
                    totalHeight += childBounds[DIM_HEIGHT];
                    if (i == 0) totalWidth = childBounds[DIM_WIDTH];
                } else {
                    totalWidth += childBounds[DIM_WIDTH];
                    if (i == 0) totalHeight = childBounds[DIM_HEIGHT];
                }
                
                recycler.recycleView(child);
            } catch (IndexOutOfBoundsException e) {
                break;
            }
        }

        int finalWidth = resolveSize(totalWidth, widthSize, widthMode, getPaddingLeft() + getPaddingRight());
        int finalHeight = resolveSize(totalHeight, heightSize, heightMode, getPaddingTop() + getPaddingBottom());

        setMeasuredDimension(finalWidth, finalHeight);
    }

    private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, boolean vertical) {
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        
        calculateItemDecorationsForChild(child, decorationRect);
        
        int hDecor = decorationRect.left + decorationRect.right;
        int vDecor = decorationRect.top + decorationRect.bottom;
        int hMargin = lp.leftMargin + lp.rightMargin;
        int vMargin = lp.topMargin + lp.bottomMargin;

        int childWidthSpec = getChildMeasureSpec(
                widthSpec, getPaddingLeft() + getPaddingRight() + hMargin + hDecor, lp.width, canScrollHorizontally());
        int childHeightSpec = getChildMeasureSpec(
                heightSpec, getPaddingTop() + getPaddingBottom() + vMargin + vDecor, lp.height, canScrollVertically());

        child.measure(childWidthSpec, childHeightSpec);

        childBounds[DIM_WIDTH] = getDecoratedMeasuredWidth(child) + lp.leftMargin + lp.rightMargin;
        childBounds[DIM_HEIGHT] = getDecoratedMeasuredHeight(child) + lp.topMargin + lp.bottomMargin;
    }

    private int resolveSize(int contentSize, int maxSize, int mode, int padding) {
        if (mode == View.MeasureSpec.EXACTLY) {
            return maxSize;
        }
        int desired = contentSize + padding;
        if (mode == View.MeasureSpec.AT_MOST) {
            return Math.min(desired, maxSize);
        }
        return desired;
    }
}

Assign the custom manager during initialization and disable nested scrolling to ensure the parent ScrollView handles all touch events:

RecyclerView recyclerView = findViewById(R.id.nestedRecycler);
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(recyclerView));
recyclerView.setAdapter(new MyCustomAdapter(dataList));

This implementation bypasses the default height-capping behavior by explicitly computing the cumulative dimensions of all adapter items. It correctly integrates ItemDecoration spacing and layout margins into the measurement pass, eliminating blank areas and truncation. Disabling nested scrolling on the RecyclerView ensures gesture handling remains consistnet within the parent container.

Tags: Android RecyclerView ScrollView Custom LayoutManager UI Measurement

Posted on Sun, 02 Aug 2026 16:39:48 +0000 by cmaclennan