ScrollView里面的RecyclerView不起作用

我想实现一个包含RecyclerView和ScrollView在同一个布局的布局。

布局模板:

<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. 最初的想法是实现这个viewGroup使用RecyclerView而不是ScrollView ,其中一个视图types是CardView但我得到了与ScrollView完全相同的结果

使用NestedScrollView而不是ScrollView

虽然这个build议

你不应该把一个可滚动视图放在另一个可滚动视图中

是一个合理的build议,但是如果你设置一个固定的高度在回收视图应该工作正常。

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

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

我知道我迟到了游戏,但问题仍然存在,即使谷歌已经修复了android.support.v7.widget.RecyclerView

我现在得到的问题是RecyclerViewlayout_height=wrap_content没有考虑到所有项目问题的ScrollView里面发生在棉花糖和牛轧糖+(API 23,24,25)版本的高度。
(更新:用android.support.v4.widget.NestedScrollView代替ScrollView在所有版本上都能正常工作,不知怎的,我错过了testing可接受的解决scheme,在我的github项目中添加了这个demo。)

尝试不同的事情后,我find了解决这个问题的解决方法。

这里是我的布局结构简而言之:

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

解决方法是用RelativeLayout包装RecyclerView 。 不要问我怎么find这个解决方法! ¯\_(ツ)_/¯

  <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

下面是一个演示屏幕截图,显示修复的实际操作:

截屏

新的Android支持库23.2解决了这个问题,你现在可以设置wrap_content作为你的RecyclerView的高度,并正常工作。

Android支持库23.2

如果为RecyclerView设置固定的高度对某人(比如我)不起作用,下面是我添加到固定高度解决scheme的内容:

 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) { } }); 

只要不滚动,RecyclerView就可以放入ScrollView。 在这种情况下,使其成为固定的高度是有意义的。

正确的解决scheme是在RecyclerView高度上使用wrap_content ,然后实现可以正确处理包装的自定义LinearLayoutManager。

将此LinearLayoutManager复制到您的项目中: https : //github.com/service/android-linear-layout-manager/blob/master/lib/src/main/java/org/solovyev/android/views/llm/LinearLayoutManager.java

然后包装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的触摸事件。 我的解决scheme只是把RecyclerView放在一起,然后用LinearLayout,以编程方式膨胀子视图,并将它们添加到布局。

对于ScrollView ,您可以使用fillViewport=true并使layout_height="match_parent"如下,并将回收视图放在里面:

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

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

手动计算RecyclerView的高度并不好,最好是使用自定义的LayoutManager

上述问题的原因是任何有滚动的视图( ListViewGridViewRecyclerView )无法计算它的高度时,添加为另一个视图中的孩子有滚动。 所以重写onMeasure方法将解决这个问题。

请用下面的代码来replace默认的布局pipe理器:

 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(); if (exactWidth && exactHeight) { // in case of exact calculations for both dimensions let's use default "onMeasure" implementation 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 (fe 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"); } } } 

更新:这个答案已经过时了,因为像NestedScrollView和RecyclerView这样的小部件支持嵌套滚动。

你不应该把一个可滚动的视图放在另一个可滚动视图中!

我build议你做主要的布局回收视图,并把你的意见作为回收视图的项目。

看看这个例子,它展示了如何在回收器视图适配器内使用多个视图。 链接到例子

我遇到了同样的问题。 这就是我所尝试的,它的工作原理。 我分享我的XML和Java代码。 希望这会帮助别人。

这是xml

 <?xml version="1.0" encoding="utf-8"?> 
 <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/iv_thumbnail" android:layout_width="match_parent" android:layout_height="200dp" /> <TextView android:id="@+id/tv_description" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Description" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Buy" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Reviews" /> <android.support.v7.widget.RecyclerView android:id="@+id/rc_reviews" android:layout_width="match_parent" android:layout_height="wrap_content"> </android.support.v7.widget.RecyclerView> </LinearLayout> </ScrollView> 

这里是相关的java代码。 它像一个魅力。

 LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(linearLayoutManager); recyclerView.setNestedScrollingEnabled(false); 

你可以使用这种方式:

将此行添加到您的recyclerView xml视图中:

  android:nestedScrollingEnabled="false" 

尝试一下,recyclerview会顺利滚动,灵活高度

希望这有助于。

看来, NestedScrollView确实解决了这个问题。 我testing过使用这个布局:

 <android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" 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:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/dummy_text" /> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" > <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content"/> </android.support.v7.widget.CardView> </LinearLayout> 

它没有问题

其实RecyclerView的主要目的是弥补ListViewScrollView 。 而不是做你实际做的事情:在ScrollView有一个RecyclerView ,我build议只有一个RecyclerView可以处理多种types的孩子。

首先,您应该使用NestedScrollView而不是ScrollView ,并将RecyclerView放入NestedScrollView

使用自定义布局类来测量屏幕的高度和宽度:

 public class CustomLinearLayoutManager extends LinearLayoutManager { public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } private int[] mMeasuredDimension = new int[2]; @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); int width = 0; int height = 0; for (int i = 0; i < getItemCount(); i++) { if (getOrientation() == HORIZONTAL) { measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), heightSpec, mMeasuredDimension); width = width + mMeasuredDimension[0]; if (i == 0) { height = mMeasuredDimension[1]; } } else { measureScrapChild(recycler, i, widthSpec, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension); height = height + mMeasuredDimension[1]; if (i == 0) { width = mMeasuredDimension[0]; } } } switch (widthMode) { case View.MeasureSpec.EXACTLY: width = widthSize; case View.MeasureSpec.AT_MOST: case View.MeasureSpec.UNSPECIFIED: } switch (heightMode) { case View.MeasureSpec.EXACTLY: height = heightSize; case View.MeasureSpec.AT_MOST: case View.MeasureSpec.UNSPECIFIED: } setMeasuredDimension(width, height); } private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] measuredDimension) { View view = recycler.getViewForPosition(position); recycler.bindViewToPosition(view, position); if (view != null) { RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams(); int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, getPaddingLeft() + getPaddingRight(), p.width); int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, getPaddingTop() + getPaddingBottom(), p.height); view.measure(childWidthSpec, childHeightSpec); measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin; measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin; recycler.recycleView(view); } } } 

并在RecyclerView的activity / fragment中实现下面的代码:

  final CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(mAdapter); recyclerView.setNestedScrollingEnabled(false); // Disables scrolling for RecyclerView, CustomLinearLayoutManager used instead of MyLinearLayoutManager recyclerView.setHasFixedSize(false); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int visibleItemCount = layoutManager.getChildCount(); int totalItemCount = layoutManager.getItemCount(); int lastVisibleItemPos = layoutManager.findLastVisibleItemPosition(); Log.i("getChildCount", String.valueOf(visibleItemCount)); Log.i("getItemCount", String.valueOf(totalItemCount)); Log.i("lastVisibleItemPos", String.valueOf(lastVisibleItemPos)); if ((visibleItemCount + lastVisibleItemPos) >= totalItemCount) { Log.i("LOG", "Last Item Reached!"); } } }); 

如果RecyclerView在ScrollView中只显示一行。 您只需要将行的高度设置为android:layout_height="wrap_content"

您也可以重写LinearLayoutManager以使回滚视图顺利滚动

 @Override public boolean canScrollVertically(){ return false; } 

对不起,迟到了,但似乎有另一种解决scheme,完全适用于你提到的情况。

如果您在回收站视图中使用回收站视图,它似乎工作得很好。 我亲自尝试过,使用它,似乎没有缓慢,没有任何杂乱。 现在我不确定这是否是一个好的做法,但嵌套多个回收站的意见,甚至嵌套滚动视图减慢。 但是这似乎很好地工作。 请尝试一下。 我相信嵌套是完全正确的。

**解决scheme,为我工作
使用带有高度的NestedScrollView作为wrap_content
使用android:layout_width =“match_parent”的RecyclerView
机器人:layout_height = “WRAP_CONTENT”
机器人:nestedScrollingEnabled = “假”
app:layoutManager =“android.support.v7.widget.LinearLayoutManager”tools:targetApi =“lollipop”

和查看持有人布局
机器人:layout_width = “match_parent”
android:layout_height =“wrap_content”//你的行内容在这里

尝试这个。 很迟才回答。 但是,将来肯定会帮助任何人。

将您的Scrollview设置为NestedScrollView

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

在你的Recyclerview

 recyclerView.setNestedScrollingEnabled(false); recyclerView.setHasFixedSize(false); 

这里是我的工作解决scheme:为RecyclerView更好地计算内容的高度和设置固定的高度。

  <ScrollView android:id="@+id/main_scroll_container" android:fillViewport="true" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:orientation="vertical" android:layout_height="wrap_content"> <android.support.v7.widget.RecyclerView android:id="@+id/product_results_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:persistentDrawingCache="none" android:alwaysDrawnWithCache="false" android:scrollbars="vertical"/> </LinearLayout> </ScrollView>