Posterior a de esta prolongada selección de datos pudimos resolver esta dificultad que suelen tener algunos usuarios. Te dejamos la solución y esperamos serte de mucha apoyo.
Solución:
Implementé la aplicación HelloWorld simple, que muestra una lista de ciudades y, según la cantidad de museos que tiene, muestra una tarjeta de ciudad en tamaño completo o la versión centrada y envuelta de la misma.
(Sí, no soy exactamente bueno en las artes :-))
Así es como lo hice.
TL; DR:
La parte crucial es ItemDecoration
: establezca la compensación adecuada de los elementos y obtendrá lo que necesita; Así es como lo he hecho:
RecyclerView recyclerViewMuseum = (RecyclerView)findViewById(R.id.recyclerViewMuseum);
recyclerViewMuseum.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
recyclerViewMuseum.setAdapter(adapter);
recyclerViewMuseum.addItemDecoration(new RecyclerView.ItemDecoration()
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
if (view instanceof CityWithOneMuseumCardView)
int totalWidth = parent.getWidth();
int cardWidth = getResources().getDimensionPixelOffset(R.dimen.small_card_width);
int sidePadding = (totalWidth - cardWidth) / 2;
sidePadding = Math.max(0, sidePadding);
outRect.set(sidePadding, 0, sidePadding, 0);
);
Aquí está mi modelo: City
y Museum
clases:
public class Museum
public String title;
public Museum(String title)
this.title = title;
public class City
public String title;
public int imageRes;
public List museums = new ArrayList<>();
public City(String title, int imageRes)
this.title = title;
this.imageRes = imageRes;
Luego Vistas: CityWithManyMuseumsCardView
y CityWithOneMuseumCardView
. Ambos están usando la interfaz de ayuda IItemDisplayer
.
public class CityWithOneMuseumCardView extends CardView implements IItemDisplayer
public CityWithOneMuseumCardView(Context context)
super(context);
LayoutInflater.from(context).inflate(R.layout.one_museum_layout, this);
@Override
public void displayItem(City city)
TextView cityTitleTextView = (TextView)findViewById(R.id.cityTitleTextView);
cityTitleTextView.setText(city.title);
public class CityWithManyMuseumsCardView extends CardView implements IItemDisplayer
public CityWithManyMuseumsCardView(Context context)
super(context);
LayoutInflater.from(context).inflate(R.layout.many_museums_layout, this);
@Override
public void displayItem(City city)
ImageView cityBackgroundImageView = (ImageView)findViewById(R.id.cityBackgroundImageView);
cityBackgroundImageView.setImageResource(city.imageRes);
TextView cityTitleTextView = (TextView)findViewById(R.id.cityTitleTextView);
cityTitleTextView.setText(city.title);
TextView cityNumberOrMuseumsTextView = (TextView)findViewById(R.id.cityNumberOrMuseumsTextView);
cityNumberOrMuseumsTextView.setText(String.valueOf(city.museums.size()));
public interface IItemDisplayer
public void displayItem(TItem item);
Y sus diseños:
Entonces necesitamos crear un adaptador para nuestro RecyclerView
CityAdapter.java
public class CityAdapter extends RecyclerView.Adapter
final static int ITEM_TYPE_MANY_MUSEUMS = 0;
final static int ITEM_TYPE_ONE_MUSEUM = 1;
private List items;
public CityAdapter(List items)
this.items = items;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType)
switch (viewType)
case ITEM_TYPE_MANY_MUSEUMS:
return new ViewHolder(new CityWithManyMuseumsCardView(viewGroup.getContext()));
case ITEM_TYPE_ONE_MUSEUM:
return new ViewHolder(new CityWithOneMuseumCardView(viewGroup.getContext()));
default:
throw new IllegalArgumentException(String.format("Unexpected viewType: %d", viewType));
@Override
public int getItemViewType(int position) items.size() < position)
throw new IllegalArgumentException("Wrong position!");
if (items.get(position).museums.size() > 1)
return ITEM_TYPE_MANY_MUSEUMS;
else if (items.get(position).museums.size() == 1)
return ITEM_TYPE_ONE_MUSEUM;
throw new IllegalArgumentException("Wrong number of museums!");
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
((IItemDisplayer) holder.itemView).displayItem(items.get(position));
@Override
public int getItemCount()
return items.size();
public class ViewHolder extends RecyclerView.ViewHolder
public ViewHolder(View itemView)
super(itemView);
He subido este proyecto a mi Dropbox. ¡No dudes en echarle un vistazo! Espero eso ayude.
Puedes probar esto:
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Field;
/**
* @link android.support.v7.widget.LinearLayoutManager which wraps its content. Note that this class will always
* wrap the content regardless of @link android.support.v7.widget.RecyclerView layout parameters.
*
* Now it's impossible to run add/remove animations with child views which have arbitrary dimensions (height for
* VERTICAL orientation and width for HORIZONTAL). However if child views have fixed dimensions
* @link #setChildSize(int) method might be used to let the layout manager know how big they are going to be.
* If animations are not used at all then a normal measuring procedure will run and child views will be measured during
* the measure pass.
*/
public class WrapContentLinearLayoutManager 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 WrapContentLinearLayoutManager(Context context)
super(context);
this.view = null;
@SuppressWarnings("UnusedDeclaration")
public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
this.view = null;
@SuppressWarnings("UnusedDeclaration")
public WrapContentLinearLayoutManager(RecyclerView view)
super(view.getContext());
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
@SuppressWarnings("UnusedDeclaration")
public WrapContentLinearLayoutManager(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
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 (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)
private void logMeasureWarning(int child)
if (BuildConfig.DEBUG)
Log.w("LinearLayoutManager", "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) 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("LinearLayoutManager", "LinearLayoutManager 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("LinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
Ponga este WrapContentLinearLayoutManager en su vista de reciclador
mRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity));
Y esto en el xml (con el relativo en el padre)
Esto debería funcionar 🙂
Recuerda algo, que puedes optar por la opción de valorar esta noticia si te fue útil.