Saltar al contenido

Centrar un CardView en un RecyclerView con un solo elemento

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.

ingrese la descripción de la imagen aquí

(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 RecyclerViewCityAdapter.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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *