RecyclerView dentro de ScrollView no funciona

Estoy tratando de implementar un diseño que contenga RecyclerView y ScrollView en el mismo diseño.

Plantilla de diseño:

  ...    

Problemas: puedo desplazarme hasta el último elemento de ScrollView

Cosas que intenté:

  1. vista de tarjeta dentro de ScrollView (ahora ScrollView contiene RecyclerView ): puede ver la tarjeta hasta el RecyclerView
  2. La idea inicial fue implementar este grupo de viewGroup usando RecyclerView lugar de ScrollView donde uno de sus tipos de vistas es CardView pero obtuve los mismos resultados que con ScrollView

usa NestedScrollView lugar de ScrollView

Consulte el documento de referencia de NestedScrollView para obtener más información.

y agregue recyclerView.setNestedScrollingEnabled(false); a su RecyclerView

Sé que estoy atrasado en el juego, pero el problema persiste incluso después de que Google haya corregido el android.support.v7.widget.RecyclerView

El problema que tengo ahora es RecyclerView con layout_height=wrap_content no toma la altura de todos los elementos dentro de ScrollView que solo ocurre en las versiones Marshmallow y Nougat + (API 23, 24, 25).
(ACTUALIZACIÓN: Reemplazar ScrollView con android.support.v4.widget.NestedScrollView funciona en todas las versiones. De alguna manera me perdí la prueba de la solución aceptada . Agregué esto en mi proyecto github como demo).

Después de probar diferentes cosas, he encontrado una solución que soluciona este problema.

Aquí está mi estructura de diseño en pocas palabras:

   (vertical - this is the only child of scrollview)   (layout_height=wrap_content)  

La solución alternativa es ajustar el RecyclerView con RelativeLayout . ¡No me preguntes cómo encontré esta solución! ¯\_(ツ)_/¯

    

El ejemplo completo está disponible en el proyecto GitHubhttps://github.com/amardeshbd/android-recycler-view-wrap-content

Aquí hay un screencast de demostración que muestra la corrección en acción:

Screencast

Aunque la recomendación de que

nunca debe poner una vista desplazable dentro de otra vista desplazable

Es un buen consejo, sin embargo, si establece una altura fija en la vista del reciclador debería funcionar bien.

Si conoce la altura del diseño del elemento del adaptador, puede calcular la altura del RecyclerView.

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

La nueva biblioteca de soporte de Android 23.2 resuelve ese problema, ahora puede establecer wrap_content como el alto de su RecyclerView y funciona correctamente.

Biblioteca de soporte de Android 23.2

En el caso de que configurar la altura fija para RecyclerView no funcionó para alguien (como yo), esto es lo que agregué a la solución de altura fija:

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

RecyclerViews está bien para poner en ScrollViews siempre que no se desplacen por sí mismos. En este caso, tiene sentido hacerlo de altura fija.

La solución adecuada es usar wrap_content en la altura de RecyclerView y luego implementar un LinearLayoutManager personalizado que pueda manejar adecuadamente la envoltura.

Copie este LinearLayoutManager en su proyecto: https://github.com/serso/android-linear-layout-manager/blob/master/lib/src/main/java/org/solovyev/android/views/llm/LinearLayoutManager.java

Luego, envuelva el RecyclerView:

  

Y configúralo así:

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

Editar: Esto puede causar complicaciones con el desplazamiento porque el RecyclerView puede robar los eventos táctiles de ScrollView. Mi solución fue simplemente deshacerme de RecyclerView en todo e ir con LinearLayout, inflar las subvistas de forma programática y agregarlas al diseño.

Para ScrollView , puede usar fillViewport=true y make layout_height="match_parent" como se muestra a continuación y poner la vista de recycler dentro:

    

No se necesitan ajustes de altura adicionales a través del código.

Calcular la altura de RecyclerView manualmente no es bueno, mejor es usar un LayoutManager personalizado.

La razón del problema anterior es cualquier vista que tenga su desplazamiento ( ListView , GridView , RecyclerView ) no pudo calcular su altura cuando se agrega como un elemento secundario en otra vista tiene desplazamiento. Por lo tanto, anular su método onMeasure resolverá el problema.

Reemplace el administrador de diseño predeterminado por el siguiente:

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

Prueba esto. Muy tarde respuesta. Pero seguramente ayudarás a cualquiera en el futuro.

Establezca su vista de desplazamiento en NestedScrollView

     

En tu Recyclerview

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

ACTUALIZACIÓN: esta respuesta está desactualizada ahora ya que hay widgets como NestedScrollView y RecyclerView que admiten el desplazamiento nested.

¡nunca debes poner una vista desplazable dentro de otra vista desplazable!

Le sugiero que haga su vista de reciclador de diseño principal y ponga sus vistas como elementos de la vista de reciclador.

Eche un vistazo a este ejemplo que muestra cómo usar múltiples vistas dentro del adaptador de vista de recycler. enlace al ejemplo

Puedes usar de esta manera:

Agregue esta línea a su vista recyclerView xml:

  android:nestedScrollingEnabled="false" 

pruébalo, recyclerview se desplazará suavemente con altura flexible

Espero que esto haya ayudado.

Parece que NestedScrollView resuelve el problema. He probado usando este diseño:

        

Y funciona sin problemas

Estaba teniendo el mismo problema. Eso es lo que intenté y funciona. Estoy compartiendo mi código xml y java. Espero que esto ayude a alguien.

Aquí está el xml

  

           

Aquí está el código de Java relacionado. Funciona a las mil maravillas.

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

Esto hace el truco:

 recyclerView.setNestedScrollingEnabled(false); 

En realidad, el objective principal de RecyclerView es compensar ListView y ScrollView . En lugar de hacer lo que realmente está haciendo: tener un RecyclerView en ScrollView , sugeriría tener solo un RecyclerView que pueda manejar muchos tipos de niños.

Primero debe usar NestedScrollView lugar de ScrollView y colocar RecyclerView dentro de NestedScrollView .

Use la clase de diseño personalizado para medir el alto y el ancho de la pantalla:

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

E implemente el código siguiente en la actividad / fragmento de RecyclerView :

  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!"); } } }); 

Si RecyclerView muestra solo una fila dentro de ScrollView. Solo necesita establecer la altura de su fila en android:layout_height="wrap_content" .

también puede anular LinearLayoutManager para hacer que recyclerview ruede sin problemas

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

Lamento haber llegado tarde a la fiesta, pero parece que hay otra solución que funciona perfectamente para el caso que mencionas.

Si usa una vista de reciclador dentro de una vista de reciclador, parece funcionar perfectamente bien. Personalmente lo probé y lo utilicé, y parece no dar lentitud ni sacudidas en absoluto. Ahora no estoy seguro de si esta es una buena práctica o no, pero anidando varias vistas de reciclado, incluso la vista de desplazamiento nested se ralentiza. Pero esto parece funcionar bien. Por favor inténtalo. Estoy seguro de que anidar va a estar perfectamente bien con esto.

** Solución que funcionó para mí
Use NestedScrollView con height como wrap_content

 
RecyclerView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" tools:targetApi="lollipop"

and view holder layout
android:layout_width="match_parent"
android:layout_height="wrap_content"

// El contenido de tu fila va aquí