RecyclerView GridLayoutManager: ¿cómo detectar automáticamente el recuento de intervalos?

110

Usando el nuevo GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Se necesita un recuento de tramos explícito, por lo que el problema ahora es: ¿cómo saber cuántos "tramos" caben por fila? Esta es una cuadrícula, después de todo. Debe haber tantos tramos como el RecyclerView pueda caber, según el ancho medido.

Usando el antiguo GridView, simplemente establecería la propiedad "columnWidth" y automáticamente detectaría cuántas columnas caben. Esto es básicamente lo que quiero replicar para RecyclerView:

  • agregar OnLayoutChangeListener en el RecyclerView
  • en esta devolución de llamada, infle un solo 'elemento de cuadrícula' y mida
  • spanCount = recicladorViewWidth / singleItemWidth;

Esto parece un comportamiento bastante común, entonces, ¿hay una forma más sencilla de que no vea?

foo64
fuente

Respuestas:

122

Personalmente, no me gusta subclasificar RecyclerView para esto, porque para mí parece que GridLayoutManager tiene la responsabilidad de detectar el recuento de intervalos. Entonces, después de investigar un poco el código fuente de Android para RecyclerView y GridLayoutManager, escribí mi propia clase GridLayoutManager extendida que hace el trabajo:

public class GridAutofitLayoutManager extends GridLayoutManager
{
    private int columnWidth;
    private boolean isColumnWidthChanged = true;
    private int lastWidth;
    private int lastHeight;

    public GridAutofitLayoutManager(@NonNull final Context context, final int columnWidth) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    public GridAutofitLayoutManager(
        @NonNull final Context context,
        final int columnWidth,
        final int orientation,
        final boolean reverseLayout) {

        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1, orientation, reverseLayout);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    private int checkedColumnWidth(@NonNull final Context context, final int columnWidth) {
        if (columnWidth <= 0) {
            /* Set default columnWidth value (48dp here). It is better to move this constant
            to static constant on top, but we need context to convert it to dp, so can't really
            do so. */
            columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                    context.getResources().getDisplayMetrics());
        }
        return columnWidth;
    }

    public void setColumnWidth(final int newColumnWidth) {
        if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
            columnWidth = newColumnWidth;
            isColumnWidthChanged = true;
        }
    }

    @Override
    public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) {
        final int width = getWidth();
        final int height = getHeight();
        if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
            final int totalSpace;
            if (getOrientation() == VERTICAL) {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            } else {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            final int spanCount = Math.max(1, totalSpace / columnWidth);
            setSpanCount(spanCount);
            isColumnWidthChanged = false;
        }
        lastWidth = width;
        lastHeight = height;
        super.onLayoutChildren(recycler, state);
    }
}

En realidad, no recuerdo por qué elegí establecer el recuento de intervalos en onLayoutChildren, escribí esta clase hace algún tiempo. Pero el punto es que debemos hacerlo después de medir la vista. para que podamos obtener su altura y ancho.

EDITAR 1: Soluciona el error en el código causado por configurar incorrectamente el recuento de intervalos. Gracias al usuario @Elyees Abouda por informar y sugerir una solución .

EDITAR 2: Algunas pequeñas refactorizaciones y arreglos de bordes con manejo manual de cambios de orientación. Gracias usuario @tatarize por informar y sugerir una solución .

s.maks
fuente
8
Esta debería ser la respuesta aceptada, el LayoutManagertrabajo de los niños es dejar a los niños y no RecyclerView's
mewa
3
@ s.maks: quiero decir, columnWidth diferiría dependiendo de los datos pasados ​​al adaptador. Digamos que si se pasan palabras de cuatro letras, entonces debería acomodar cuatro elementos seguidos. Si se pasan palabras de 10 letras, entonces debería poder acomodar solo 2 elementos seguidos.
Shubham
2
Para mí, al rotar el dispositivo, está cambiando un poco el tamaño de la cuadrícula, encogiéndose 20dp. ¿Tienes alguna información sobre esto? Gracias.
Alexandre
3
A veces, getWidth()o getHeight()es 0 antes de que se cree la vista, lo que obtendrá un spanCount incorrecto (1 ya que totalSpace será <= 0). Lo que agregué es ignorar setSpanCount en este caso. ( onLayoutChildrense llamará de nuevo más tarde)
Elyess Abouda
3
Hay una condición de borde que no está cubierta. Si configura configChanges de manera que maneje la rotación en lugar de dejar que reconstruya toda la actividad, tiene el extraño caso de que el ancho de la vista de reciclaje cambia, mientras que nada más lo hace. Con el ancho y la altura cambiados, el spancount está sucio, pero mColumnWidth no ha cambiado, por lo que onLayoutChildren () aborta y no recalcula los valores ahora sucios. Guarde la altura y la anchura anteriores, y active si cambia de forma distinta de cero.
Tatarizar
29

Lo logré usando un observador de árbol de vista para obtener el ancho de la vista de recylcer una vez renderizada y luego obteniendo las dimensiones fijas de la vista de mi tarjeta de los recursos y luego estableciendo el recuento de intervalo después de hacer mis cálculos. Solo es realmente aplicable si los elementos que está mostrando tienen un ancho fijo. Esto me ayudó a rellenar automáticamente la cuadrícula independientemente del tamaño de la pantalla o la orientación.

mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mRecyclerView.getViewTreeObserver().removeOnGLobalLayoutListener(this);
                    int viewWidth = mRecyclerView.getMeasuredWidth();
                    float cardViewWidth = getActivity().getResources().getDimension(R.dimen.cardview_layout_width);
                    int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
                    mLayoutManager.setSpanCount(newSpanCount);
                    mLayoutManager.requestLayout();
                }
            });
speedynomads
fuente
2
Usé esto y obtuve ArrayIndexOutOfBoundsException( en android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) al desplazarme por el archivoRecyclerView .
fikr4n
Simplemente agregue mLayoutManager.requestLayout () después de setSpanCount () y funcionará
alvinmeimoun
2
Nota: removeGlobalOnLayoutListener()está obsoleto en el nivel 16 de API removeOnGlobalLayoutListener(). Documentación .
pez
error tipográfico removeOnGLobalLayoutListener debe eliminarseOnGlobalLayoutListener
Theo
17

Bueno, esto es lo que usé, bastante básico, pero hace el trabajo por mí. Este código básicamente obtiene el ancho de la pantalla en caídas y luego lo divide por 300 (o el ancho que esté usando para el diseño de su adaptador). Así que los teléfonos más pequeños con un ancho de inmersión de 300-500 solo muestran una columna, las tabletas 2-3 columnas, etc. Sencillo, sin complicaciones y sin inconvenientes, por lo que puedo ver.

Display display = getActivity().getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);

float density  = getResources().getDisplayMetrics().density;
float dpWidth  = outMetrics.widthPixels / density;
int columns = Math.round(dpWidth/300);
mLayoutManager = new GridLayoutManager(getActivity(),columns);
mRecyclerView.setLayoutManager(mLayoutManager);
Costillas
fuente
1
¿Por qué usar el ancho de la pantalla en lugar del ancho de RecyclerView? Y codificar 300 es una mala práctica (debe mantenerse sincronizado con su diseño xml)
foo64
@ foo64 en el xml, puede simplemente establecer match_parent en el elemento. Pero sí, todavía es feo;)
Philip Giuliani
15

Extendí RecyclerView y anulé el método onMeasure.

Establecí un ancho de elemento (variable de miembro) lo antes posible, con un valor predeterminado de 1. Esto también se actualiza en la configuración cambiada. Esto ahora tendrá tantas filas como puedan caber en retrato, paisaje, teléfono / tableta, etc.

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    super.onMeasure(widthSpec, heightSpec);
    int width = MeasureSpec.getSize(widthSpec);
    if(width != 0){
        int spans = width / mItemWidth;
        if(spans > 0){
            mLayoutManager.setSpanCount(spans);
        }
    }
}
andrew_s
fuente
12
+1 Chiu-ki Chan tiene una publicación de blog que describe este enfoque y también un proyecto de muestra .
CommonsWare
7

Estoy publicando esto en caso de que alguien obtenga un ancho de columna extraño como en mi caso.

No puedo comentar sobre la respuesta de @ s-marks debido a mi baja reputación. Apliqué su solución , pero obtuve un ancho de columna extraño, así que modifiqué la función CheckColumnWidth de la siguiente manera:

private int checkedColumnWidth(Context context, int columnWidth)
{
    if (columnWidth <= 0)
    {
        /* Set default columnWidth value (48dp here). It is better to move this constant
        to static constant on top, but we need context to convert it to dp, so can't really
        do so. */
        columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                context.getResources().getDisplayMetrics());
    }

    else
    {
        columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, columnWidth,
                context.getResources().getDisplayMetrics());
    }
    return columnWidth;
}

Al convertir el ancho de columna dado en DP, se solucionó el problema.

Ariq
fuente
2

Para acomodar el cambio de orientación en la respuesta de s-marks , agregué una marca en el cambio de ancho (ancho de getWidth (), no ancho de columna).

private boolean mWidthChanged = true;
private int mWidth;


@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
{
    int width = getWidth();
    int height = getHeight();

    if (width != mWidth) {
        mWidthChanged = true;
        mWidth = width;
    }

    if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0
            || mWidthChanged)
    {
        int totalSpace;
        if (getOrientation() == VERTICAL)
        {
            totalSpace = width - getPaddingRight() - getPaddingLeft();
        }
        else
        {
            totalSpace = height - getPaddingTop() - getPaddingBottom();
        }
        int spanCount = Math.max(1, totalSpace / mColumnWidth);
        setSpanCount(spanCount);
        mColumnWidthChanged = false;
        mWidthChanged = false;
    }
    super.onLayoutChildren(recycler, state);
}
Andreas
fuente
2

La solución votada está bien, pero maneja los valores entrantes como píxeles, lo que puede hacer que se tropiece si está codificando valores para probar y asumiendo dp. La forma más fácil es probablemente poner el ancho de la columna en una dimensión y leerlo al configurar GridAutofitLayoutManager, que convertirá automáticamente dp en el valor de píxel correcto:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
toncek
fuente
sí, eso me dejó perplejo. realmente sería simplemente aceptar el recurso en sí. Quiero decir, eso es lo que siempre haremos.
Tatarizar
2
  1. Establecer un ancho fijo mínimo de imageView (144dp x 144dp por ejemplo)
  2. Cuando crea GridLayoutManager, necesita saber cuántas columnas serán con el tamaño mínimo de imageView:

    WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); //Получаем размер экрана
    Display display = wm.getDefaultDisplay();
    
    Point point = new Point();
    display.getSize(point);
    int screenWidth = point.x; //Ширина экрана
    
    int photoWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 144, this.getResources().getDisplayMetrics()); //Переводим в точки
    
    int columnsCount = screenWidth/photoWidth; //Число столбцов
    
    GridLayoutManager gridLayoutManager = new GridLayoutManager(this, columnsCount);
    recyclerView.setLayoutManager(gridLayoutManager);
  3. Después de eso, debe cambiar el tamaño de imageView en el adaptador si tiene espacio en la columna. Puede enviar newImageViewSize y luego inisilizar el adaptador de la actividad allí, calcula la pantalla y el recuento de columnas:

    @Override //Заполнение нашей плитки
    public void onBindViewHolder(PhotoHolder holder, int position) {
       ...
       ViewGroup.LayoutParams photoParams = holder.photo.getLayoutParams(); //Параметры нашей фотографии
    
       int newImageViewSize = screenWidth/columnsCount; //Новый размер фотографии
    
       photoParams.width = newImageViewSize; //Установка нового размера
       photoParams.height = newImageViewSize;
       holder.photo.setLayoutParams(photoParams); //Установка параметров
       ...
    }

Funciona en ambas orientaciones. En vertical tengo 2 columnas y en horizontal - 4 columnas. El resultado: https://i.stack.imgur.com/WHvyD.jpg

Serjant.arbuz
fuente
2

La conclusión anterior responde aquí

Omid Raha
fuente
1

Esta es la clase de s.maks con una pequeña corrección para cuando la vista del reciclador cambia de tamaño. Por ejemplo, cuando se ocupa de los cambios de orientación usted mismo (en el manifiesto android:configChanges="orientation|screenSize|keyboardHidden"), o por alguna otra razón, la vista de reciclaje podría cambiar de tamaño sin que cambie mColumnWidth. También cambié el valor int que se necesita para ser el recurso del tamaño y permití que un constructor sin recurso y luego setColumnWidth lo hiciera usted mismo.

public class GridAutofitLayoutManager extends GridLayoutManager {
    private Context context;
    private float mColumnWidth;

    private float currentColumnWidth = -1;
    private int currentWidth = -1;
    private int currentHeight = -1;


    public GridAutofitLayoutManager(Context context) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        this.context = context;
        setColumnWidthByResource(-1);
    }

    public GridAutofitLayoutManager(Context context, int resource) {
        this(context);
        this.context = context;
        setColumnWidthByResource(resource);
    }

    public GridAutofitLayoutManager(Context context, int resource, int orientation, boolean reverseLayout) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1, orientation, reverseLayout);
        this.context = context;
        setColumnWidthByResource(resource);
    }

    public void setColumnWidthByResource(int resource) {
        if (resource >= 0) {
            mColumnWidth = context.getResources().getDimension(resource);
        } else {
            /* Set default columnWidth value (48dp here). It is better to move this constant
            to static constant on top, but we need context to convert it to dp, so can't really
            do so. */
            mColumnWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                    context.getResources().getDisplayMetrics());
        }
    }

    public void setColumnWidth(float newColumnWidth) {
        mColumnWidth = newColumnWidth;
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        recalculateSpanCount();
        super.onLayoutChildren(recycler, state);
    }

    public void recalculateSpanCount() {
        int width = getWidth();
        if (width <= 0) return;
        int height = getHeight();
        if (height <= 0) return;
        if (mColumnWidth <= 0) return;
        if ((width != currentWidth) || (height != currentHeight) || (mColumnWidth != currentColumnWidth)) {
            int totalSpace;
            if (getOrientation() == VERTICAL) {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            } else {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            int spanCount = (int) Math.max(1, Math.floor(totalSpace / mColumnWidth));
            setSpanCount(spanCount);
            currentColumnWidth = mColumnWidth;
            currentWidth = width;
            currentHeight = height;
        }
    }
}
Tatarizar
fuente
-1

Establezca spanCount en un número grande (que es el número máximo de columnas) y establezca un SpanSizeLookup personalizado en GridLayoutManager.

mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int i) {
        return SPAN_COUNT / (int) (mRecyclerView.getMeasuredWidth()/ CELL_SIZE_IN_PX);
    }
});

Es un poco feo, pero funciona.

Creo que un administrador como AutoSpanGridLayoutManager sería la mejor solución, pero no encontré nada de eso.

EDITAR: Hay un error, en algún dispositivo agrega espacio en blanco a la derecha

alvinmeimoun
fuente
El espacio de la derecha no es un error. si el recuento del intervalo es 5 y getSpanSizedevuelve 3, habrá un espacio porque no está llenando el intervalo.
Nicolas Tyler
-6

Aquí están las partes relevantes de un contenedor que he estado usando para detectar automáticamente el recuento de tramos. Inicializar llamando setGridLayoutManagercon un R.layout.my_grid_item de referencia, y se da cuenta de cómo muchos de los que caben en cada fila.

public class AutoSpanRecyclerView extends RecyclerView {
    private int     m_gridMinSpans;
    private int     m_gridItemLayoutId;
    private LayoutRequester m_layoutRequester = new LayoutRequester();

    public void setGridLayoutManager( int orientation, int itemLayoutId, int minSpans ) {
        GridLayoutManager layoutManager = new GridLayoutManager( getContext(), 2, orientation, false );
        m_gridItemLayoutId = itemLayoutId;
        m_gridMinSpans = minSpans;

        setLayoutManager( layoutManager );
    }

    @Override
    protected void onLayout( boolean changed, int left, int top, int right, int bottom ) {
        super.onLayout( changed, left, top, right, bottom );

        if( changed ) {
            LayoutManager layoutManager = getLayoutManager();
            if( layoutManager instanceof GridLayoutManager ) {
                final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                LayoutInflater inflater = LayoutInflater.from( getContext() );
                View item = inflater.inflate( m_gridItemLayoutId, this, false );
                int measureSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
                item.measure( measureSpec, measureSpec );
                int itemWidth = item.getMeasuredWidth();
                int recyclerViewWidth = getMeasuredWidth();
                int spanCount = Math.max( m_gridMinSpans, recyclerViewWidth / itemWidth );

                gridLayoutManager.setSpanCount( spanCount );

                // if you call requestLayout() right here, you'll get ArrayIndexOutOfBoundsException when scrolling
                post( m_layoutRequester );
            }
        }
    }

    private class LayoutRequester implements Runnable {
        @Override
        public void run() {
            requestLayout();
        }
    }
}
foo64
fuente
1
¿Por qué votar en contra de la respuesta aceptada? debería explicar por qué downvote
androidXP