ScrollView 中的 RecyclerView 不起作用

341

我想实现一个包含RecyclerViewScrollView的布局。

布局模板:

<RelativeLayout>
    <ScrollView android:id="@+id/myScrollView">

       <unrelated data>...</unrealated data>

       <android.support.v7.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/my_recycler_view" />
    </ScrollView>   
</RelativeLayout>

问题:我无法滚动到ScrollView的最后一个元素。

尝试过的事情:

  1. ScrollView中放置卡片视图(现在ScrollView包含RecyclerView)-可以看到卡片视图,但只能滚动到RecyclerView的位置。
  2. 最初的想法是使用RecyclerView而不是ScrollView来实现此ViewGroup,其中之一的视图类型是CardView,但我得到了与ScrollView完全相同的结果。

18
在许多这类情况下,一个简单的解决方案是使用 NestedScrollView,因为它可以处理许多滚动问题。 - Richard Le Mesurier
1
Richard在二月份已经给了你答案。使用NestedScrollView代替ScrollView,这正是它的用途所在。 - Mike M.
3
对我来说没有任何变化。 - Luke Allison
2
供以后参考,如果有人遇到类似问题,仅限于棉花糖/牛轧糖(API 23、24)设备,请查看我的解决方法:https://dev59.com/il4d5IYBdhLWcg3wLf_P#38995399 - Hossain Khan
@RichardLeMesurier 这也破坏了RecyclerView的整个目的。视图不会被回收,因此您最终将拥有与适配器中项目数量相同的ViewHolder类。 - Farid
显示剩余6条评论
26个回答

816

请使用NestedScrollView代替ScrollView

更多信息请参阅NestedScrollView参考文档

并在您的RecyclerView中添加recyclerView.setNestedScrollingEnabled(false);


25
适用于:android.support.v4.widget.NestedScrollView - Cocorico
11
在为 ViewHolder 填充的布局中保留android:layout_height="wrap_content" - Sarath Babu
6
在一个复杂的布局中,相对于ScrollView,NestedScrollView在我的设备上出现了卡顿。我正在寻找一种不使用NestedScrollView的解决方案。 - Roman Samoilenko
20
你可以在XML中添加android:nestedScrollingEnabled="false",而不是使用recyclerView.setNestedScrollingEnabled(false); 这样做可以使内容更加通俗易懂,但不能改变原文的意思。 - kostyabakay
11
这适用于我,但请记住,recyclerView内部的项并没有被回收。 - Mostafa Arian Nejad
显示剩余10条评论

120

我知道我有点晚了,但即使在谷歌对android.support.v7.widget.RecyclerView进行修复后,问题仍然存在。

我现在遇到的问题是,在Marshmallow和Nougat+(API 23、24、25)版本中,layout_height=wrap_contentRecyclerViewScrollView内部不会占用所有项的高度。
(更新:将ScrollView替换为android.support.v4.widget.NestedScrollView在所有版本上都有效。我不知何故错过了测试接受的解决方案。我在我的 github 项目中添加了这个演示。)

经过尝试不同的方法,我找到了解决此问题的解决方法。

以下是我的布局结构概述:

<ScrollView>
  <LinearLayout> (vertical - this is the only child of scrollview)
     <SomeViews>
     <RecyclerView> (layout_height=wrap_content)
     <SomeOtherViews>

解决方法是将 RecyclerView 包装在 RelativeLayout 中。不要问我是如何发现这个解决方法的!!!¯\_(ツ)_/¯

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>

GitHub 上有一个完整的示例项目 - https://github.com/amardeshbd/android-recycler-view-wrap-content

以下是演示视频,显示了修复后的效果:

Screencast


7
谢谢,这对我很有帮助。但在尝试你的解决方案后,滚动变得困难了。因此我设置了 RecyclerView.setNestedScrollingEnabled(false); 现在它完美运行。 - Sagar Chavada
6
这种方法会回收视图吗?如果我们有数百个需要回收利用的对象,这只是一种hack而不是解决方案。 - androidXP
2
是的,@androidXP 是正确的,这个 hack 并不适用于长列表。我的使用情况是在一个少于10个固定项的列表视图中。至于我如何找到另一种解决方法,那是因为我尝试了很多随机的东西,这只是其中之一 :-) - Hossain Khan
3
如果我使用这个解决方案,那么onBindView将会被调用列表中的所有项目,这不是recyclerview的用例。 - thedarkpassenger
1
@HossainKhan 谢谢。没有嵌套滚动的修复方法对我有用,但值得指出的是,只有RelativeLayout有效,我测试了Linear和Constraint,由于某种原因它们不起作用。 - Raykud
显示剩余11条评论

57

尽管建议永远不要将可滚动视图置于另一个可滚动视图内是一种明智的做法,但如果您在Recycler View上设置了固定高度,它应该可以正常工作。

如果您知道适配器项布局的高度,您可以仅计算RecyclerView的高度。

int viewHeight = adapterItemSize * adapterData.size();
recyclerView.getLayoutParams().height = viewHeight;

15
如何在RecyclerView中获取adapterItemSize?有任何想法吗? - Krutik
1
它像魔法一样运行得很好! 稍作更正:int viewHeight = adapterItemSize * adapterData.size(); recyclerView.getLayoutParams().height = viewHeight; - Vaibhav Jani
5
如何找到adapterItemSize? - droidster.me
3
adapterItemSize 变量是什么? - IgorGanapolsky
2
避免在ScrollView中使用RecyclerView,因为ScrollView会给其子元素提供无限的空间。这会导致RecyclerView的高度被设置为wrap_content时,在垂直方向上测量到无限高(直到RecyclerView的最后一个项目)。不要在ScrollView中使用RecyclerView,而是使用具有不同项类型的RecyclerView。通过这种实现方式,ScrollView的子元素将作为视图类型进行处理。在RecyclerView内处理这些视图类型。 - abhishesh
显示剩余8条评论

35

如果为RecyclerView设置固定高度无效(就像我一样),这是我添加到固定高度解决方案中的内容:

mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        int action = e.getAction();
        switch (action) {
            case MotionEvent.ACTION_MOVE:
                rv.getParent().requestDisallowInterceptTouchEvent(true);
                break;
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {

    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    }
});

1
确实,这对我很有效。有了这个设置,我能够上下移动滚动视图,并且当我选择RecyclerView进行滚动时,它优先于滚动视图。感谢您的提示。 - PGMacDesign
1
它对我也起作用了,只要确保在滚动视图的直接子级上使用getParent。 - BigBen3216
该方法在CyanogenMod分发版上也适用。在CyanogenMod上的固定高度解决方案可行,但前提是所有项目的高度都需要是绝对高度,这与首次使用RecyclerView的初衷相悖。已点赞。 - HappyKatz
我也需要recyclerView.setNestedScrollingEnabled(false); - Analizer
这真的起作用了!我从来没有想过在项目级别上设置触摸监听器。我曾经尝试在recyclerView级别上设置触摸监听器,但没有成功。非常好的解决方案。干杯! - Tamoxin

28

新版 Android Support Library 23.2 解决了这个问题,现在您可以将RecyclerView的高度设置为wrap_content并正常工作。

Android Support Library 23.2


1
无法正常运行(23.4.0) - behelit
1
@behelit 23.4.0存在一些问题 https://code.google.com/p/android/issues/detail?id=210085#makechanges,请使用23.2.1代替。 - Samad
1
在25.0.1上甚至不能正常运行。 - maruti060385

18

RecyclerViews 可以放置在 ScrollViews 中,只要它们本身不是可滚动的。在这种情况下,将其设置为固定高度是有意义的。

正确的解决方法是在 RecyclerView 的高度上使用 wrap_content,然后实现一个自定义的 LinearLayoutManager,以便能够正确处理包装。

请将此 LinearLayoutManager 复制到您的项目中:链接

然后将 RecyclerView 包装起来:

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

然后按照下面这样设置:

RecyclerView list = (RecyclerView)findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new com.example.myapp.LinearLayoutManager(list.getContext()));
list.setAdapter(new MyViewAdapter(data));

编辑:这可能会导致滚动方面的问题,因为RecyclerView可以夺取ScrollView的触摸事件。我的解决方案是放弃所有内容中的RecyclerView并改用LinearLayout,通过编程方式填充子视图,并将它们添加到布局中。


4
你能否在RecyclerView上调用setNestedScrollingEnabled(false)方法? - Eshaan

17

如果使用ScrollView,您可以将fillViewport=true并将layout_height="match_parent"设置如下,并将RecyclerView放在里面:

<ScrollView
    android:fillViewport="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_below="@+id/llOptions">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rvList"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
</ScrollView>

无需通过代码进行进一步的高度调整。


已测试使用v23.2.1正常工作。之前用它来添加RecyclerView上方的布局。 - winsontan520
仅限于RecyclerView可以滚动而ScrollView无法滚动。 - Samad
不起作用,因为它会创建并绑定到RecyclerView中的所有项目。 - android developer

15
如果您将RecyclerView放在NestedScrollView中并启用recyclerView.setNestedScrollingEnabled(false);,滚动将正常工作
然而,存在一个问题

RecyclerView无法进行回收利用

例如,如果您的RecyclerView(位于NestedScrollViewScrollView中)有100个项目。
Activity启动时,将创建100个项目(100个项目的onCreateViewHolderonBindViewHolder会同时调用)。
例如,对于每个项目,您将从API加载一个大图像=>活动创建->将加载100个图像。
这使得启动活动变得缓慢而且卡顿。
可能的解决方案:
- 考虑使用具有多个类型的RecyclerView
但是,如果在您的情况下,RecyclerView中只有一些项目,并且回收与否不会对性能产生太大影响,您可以在简单情况下在ScrollView中使用RecyclerView

请展示如何使RecyclerView在ScrollView内部实现回收?谢谢! - Liar
2
@Liar,目前没有办法在将RecyclerView放入ScrollView后进行回收。如果您想要回收,请考虑另一种方法(例如使用具有多个类型的RecyclerView)。 - Linh
你可以为回收视图设置一个固定高度。 - martinseal1987

14
计算RecyclerView的高度不是很好,最好使用自定义的LayoutManager。上述问题的原因是任何具有滚动条(ListView、GridView、RecyclerView)的视图,在添加为另一个具有滚动条的视图的子视图时,无法计算其高度。因此,覆盖它的onMeasure方法将解决该问题。请使用下面的自定义布局管理器替换默认的布局管理器:
public class MyLinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {

    private static boolean canMakeInsetsDirty = true;
    private static Field insetsDirtyField = null;

    private static final int CHILD_WIDTH = 0;
    private static final int CHILD_HEIGHT = 1;
    private static final int DEFAULT_CHILD_SIZE = 100;

    private final int[] childDimensions = new int[2];
    private final RecyclerView view;

    private int childSize = DEFAULT_CHILD_SIZE;
    private boolean hasChildSize;
    private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS;
    private final Rect tmpRect = new Rect();

    @SuppressWarnings("UnusedDeclaration")
    public MyLinearLayoutManager(Context context) {
        super(context);
        this.view = null;
    }

    @SuppressWarnings("UnusedDeclaration")
    public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
        this.view = null;
    }

    @SuppressWarnings("UnusedDeclaration")
    public MyLinearLayoutManager(RecyclerView view) {
        super(view.getContext());
        this.view = view;
        this.overScrollMode = ViewCompat.getOverScrollMode(view);
    }

    @SuppressWarnings("UnusedDeclaration")
    public MyLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) {
        super(view.getContext(), orientation, reverseLayout);
        this.view = view;
        this.overScrollMode = ViewCompat.getOverScrollMode(view);
    }

    public void setOverScrollMode(int overScrollMode) {
        if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER) {
            throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode);
        }

        if (this.view == null) throw new IllegalStateException("view == null");

        this.overScrollMode = overScrollMode;
        ViewCompat.setOverScrollMode(view, overScrollMode);
    }

    public static int makeUnspecifiedSpec() {
        return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    }

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

        final int widthSize = View.MeasureSpec.getSize(widthSpec);
        final int heightSize = View.MeasureSpec.getSize(heightSpec);

        final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
        final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;

        final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
        final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;

        final int unspecified = makeUnspecifiedSpec();

        /** 
         * In case of exact calculations for both dimensions let's 
         * use default "onMeasure" implementation. 
         */
        if (exactWidth && exactHeight) {
            super.onMeasure(recycler, state, widthSpec, heightSpec);
            return;
        }

        final boolean vertical = getOrientation() == VERTICAL;

        initChildDimensions(widthSize, heightSize, vertical);

        int width = 0;
        int height = 0;

        /**
         * It's possible to get scrap views in recycler which are bound to old (invalid) 
         * adapter entities. This happens because their invalidation happens after "onMeasure" 
         * method. As a workaround let's clear the recycler now (it should not cause 
         * any performance issues while scrolling as "onMeasure" is never called whiles scrolling).
         */
        recycler.clear();

        final int stateItemCount = state.getItemCount();
        final int adapterItemCount = getItemCount();

        /**
         * Adapter always contains actual data while state might contain old data
         * (f.e. data before the animation is done).  As we want to measure the view 
         * with actual data we must use data from the adapter and not from  the state.
         */
        for (int i = 0; i < adapterItemCount; i++) {
            if (vertical) {
                if (!hasChildSize) {
                    if (i < stateItemCount) {
                        /**
                         * We should not exceed state count, otherwise we'll get IndexOutOfBoundsException. 
                         * For such items we will use previously calculated dimensions.
                         */
                        measureChild(recycler, i, widthSize, unspecified, childDimensions);
                    } else {
                        logMeasureWarning(i);
                    }
                }
                height += childDimensions[CHILD_HEIGHT];
                if (i == 0) {
                    width = childDimensions[CHILD_WIDTH];
                }
                if (hasHeightSize && height >= heightSize) {
                    break;
                }
            } else {
                if (!hasChildSize) {
                    if (i < stateItemCount) {
                        /**
                         * We should not exceed state count, otherwise we'll get IndexOutOfBoundsException. 
                         * For such items we will use previously calculated dimensions.
                         */
                        measureChild(recycler, i, unspecified, heightSize, childDimensions);
                    } else {
                        logMeasureWarning(i);
                    }
                }
                width += childDimensions[CHILD_WIDTH];
                if (i == 0) {
                    height = childDimensions[CHILD_HEIGHT];
                }
                if (hasWidthSize && width >= widthSize) {
                    break;
                }
            }
        }

        if (exactWidth) {
            width = widthSize;
        } else {
            width += getPaddingLeft() + getPaddingRight();
            if (hasWidthSize) {
                width = Math.min(width, widthSize);
            }
        }

        if (exactHeight) {
            height = heightSize;
        } else {
            height += getPaddingTop() + getPaddingBottom();
            if (hasHeightSize) {
                height = Math.min(height, heightSize);
            }
        }

        setMeasuredDimension(width, height);

        if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) {
            final boolean fit = (vertical && (!hasHeightSize || height < heightSize))
                || (!vertical && (!hasWidthSize || width < widthSize));

            ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS);
        }
    }

    private void logMeasureWarning(int child) {
        if (BuildConfig.DEBUG) {
            Log.w("MyLinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
                "To remove this message either use #setChildSize() method or don't run RecyclerView animations");
        }
    }

    private void initChildDimensions(int width, int height, boolean vertical) {
        if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
            /** Already initialized, skipping. */
            return;
        }
        if (vertical) {
            childDimensions[CHILD_WIDTH] = width;
            childDimensions[CHILD_HEIGHT] = childSize;
        } else {
            childDimensions[CHILD_WIDTH] = childSize;
            childDimensions[CHILD_HEIGHT] = height;
        }
    }

    @Override
    public void setOrientation(int orientation) {
        /** Might be called before the constructor of this class is called. */
        //noinspection ConstantConditions
        if (childDimensions != null) {
            if (getOrientation() != orientation) {
                childDimensions[CHILD_WIDTH] = 0;
                childDimensions[CHILD_HEIGHT] = 0;
            }
        }
        super.setOrientation(orientation);
    }

    public void clearChildSize() {
        hasChildSize = false;
        setChildSize(DEFAULT_CHILD_SIZE);
    }

    public void setChildSize(int childSize) {
        hasChildSize = true;
        if (this.childSize != childSize) {
            this.childSize = childSize;
            requestLayout();
        }
    }

    private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) {
        final View child;
        try {
            child = recycler.getViewForPosition(position);
        } catch (IndexOutOfBoundsException e) {
            if (BuildConfig.DEBUG) {
                Log.w("MyLinearLayoutManager", "MyLinearLayoutManager doesn't work well with animations. Consider switching them off", e);
            }
            return;
        }

        final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();

        final int hPadding = getPaddingLeft() + getPaddingRight();
        final int vPadding = getPaddingTop() + getPaddingBottom();

        final int hMargin = p.leftMargin + p.rightMargin;
        final int vMargin = p.topMargin + p.bottomMargin;

        /** We must make insets dirty in order calculateItemDecorationsForChild to work. */
        makeInsetsDirty(p);
        /** This method should be called before any getXxxDecorationXxx() methods. */
        calculateItemDecorationsForChild(child, tmpRect);

        final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
        final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);

        final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
        final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically());

        child.measure(childWidthSpec, childHeightSpec);

        dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
        dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;

        /** As view is recycled let's not keep old measured values. */
        makeInsetsDirty(p);
        recycler.recycleView(child);
    }

    private static void makeInsetsDirty(RecyclerView.LayoutParams p) {
        if (!canMakeInsetsDirty) return;

        try {
            if (insetsDirtyField == null) {
                insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty");
                insetsDirtyField.setAccessible(true);
            }
            insetsDirtyField.set(p, true);
        } catch (NoSuchFieldException e) {
            onMakeInsertDirtyFailed();
        } catch (IllegalAccessException e) {
            onMakeInsertDirtyFailed();
        }
    }

    private static void onMakeInsertDirtyFailed() {
        canMakeInsetsDirty = false;
        if (BuildConfig.DEBUG) {
            Log.w("MyLinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
        }
    }
}

当我们将数据设置到适配器时,如何调用这个类? - Milan Gajera

14

试试这个。虽然回答晚了,但肯定会帮助未来的任何人。

  1. 将您的ScrollView更改为NestedScrollView

<android.support.v4.widget.NestedScrollView>

    <android.support.v7.widget.RecyclerView 
        ...
        ... />

</android.support.v4.widget.NestedScrollView>
在您的UI代码中,更新它以适用于Recyclerview
recyclerView.setNestedScrollingEnabled(false); 
recyclerView.setHasFixedSize(false);

11
在NestedScrollView中使用RecyclerView会导致即使列表项不可见,仍会为每个项目调用onBindView方法。有没有解决这个问题的办法? - thedarkpassenger
你只需要将PaddingBottom属性赋值给嵌套在NestedScrollView内部的LinearLayout即可 - @thedarkpassenger - oalpayli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接