RecyclerView GridLayoutManager: come rilevare automaticamente il conteggio degli intervalli?


110

Utilizzando il nuovo GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Richiede un conteggio degli intervalli esplicito, quindi il problema ora diventa: come fai a sapere quanti "intervalli" si adattano per riga? Questa è una griglia, dopotutto. Dovrebbero esserci tante campate quante ne può contenere RecyclerView, in base alla larghezza misurata.

Usando il vecchio GridView, dovresti semplicemente impostare la proprietà "columnWidth" e rilevare automaticamente quante colonne si adattano. Questo è fondamentalmente ciò che voglio replicare per RecyclerView:

  • aggiungi OnLayoutChangeListener nel file RecyclerView
  • in questo callback, gonfiare un singolo "elemento della griglia" e misurarlo
  • spanCount = recyclerViewWidth / singleItemWidth;

Sembra un comportamento abbastanza comune, quindi c'è un modo più semplice che non vedo?

Risposte:


122

Personalmente non mi piace sottoclassare RecyclerView per questo, perché per me sembra che ci sia la responsabilità di GridLayoutManager di rilevare il conteggio degli intervalli. Quindi, dopo un po 'di ricerca sul codice sorgente Android per RecyclerView e GridLayoutManager, ho scritto la mia classe GridLayoutManager estesa che fa il lavoro:

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

In realtà non ricordo perché ho scelto di impostare il conteggio dello span in onLayoutChildren, ho scritto questo corso qualche tempo fa. Ma il punto è che dobbiamo farlo dopo che la vista è stata misurata. così possiamo ottenere la sua altezza e larghezza.

EDIT 1: correzione dell'errore nel codice causato dall'impostazione errata del conteggio dell'intervallo. Grazie all'utente @Elyees Abouda per aver segnalato e suggerito la soluzione .

EDIT 2: alcuni piccoli refactoring e fix edge case con gestione manuale delle modifiche dell'orientamento. Grazie all'utente @tatarize per aver segnalato e suggerito la soluzione .


8
Questa dovrebbe essere la risposta accettata, è LayoutManagercompito di licenziare i bambini e non RecyclerViewdi
mewa

3
@ s.maks: Intendo che columnWidth differirebbe a seconda dei dati passati all'adattatore. Diciamo che se vengono passate parole di quattro lettere, dovrebbe contenere quattro elementi di fila se vengono passate parole di 10 lettere, dovrebbe essere in grado di ospitare solo 2 elementi di fila.
Shubham

2
Per me, quando si ruota il dispositivo, le dimensioni della griglia cambiano leggermente, riducendosi di 20 dp. Hai qualche informazione su questo? Grazie.
Alexandre

3
A volte getWidth()o getHeight()è 0 prima della creazione della vista, che otterrà uno spanCount errato (1 poiché totalSpace sarà <= 0). Quello che ho aggiunto è ignorare setSpanCount in questo caso. ( onLayoutChildrensarà richiamato più tardi)
Elyess Abouda

3
C'è una condizione limite che non è coperta. Se imposti configChanges in modo tale da gestire la rotazione piuttosto che lasciare che ricostruisca l'intera attività, hai il caso dispari che la larghezza del recyclerview cambia, mentre nient'altro fa. Con la larghezza e l'altezza modificate, lo spancount è sporco, ma mColumnWidth non è cambiato, quindi onLayoutChildren () si interrompe e non ricalcola i valori ora sporchi. Salva l'altezza e la larghezza precedenti e attiva il trigger se cambia in modo diverso da zero.
Tatarize

29

Ho ottenuto ciò utilizzando un osservatore dell'albero di visualizzazione per ottenere la larghezza della vista di riciclo una volta renderizzata e quindi ottenere le dimensioni fisse della mia visualizzazione della scheda dalle risorse e quindi impostare il conteggio dell'intervallo dopo aver eseguito i miei calcoli. È realmente applicabile solo se gli elementi visualizzati hanno una larghezza fissa. Questo mi ha aiutato a popolare automaticamente la griglia indipendentemente dalle dimensioni o dall'orientamento dello schermo.

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

2
Ho usato questo e ho ottenuto ArrayIndexOutOfBoundsException( su android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) durante lo scorrimento del file RecyclerView.
fikr4n

Basta aggiungere mLayoutManager.requestLayout () dopo setSpanCount () e funziona
alvinmeimoun

2
Nota: removeGlobalOnLayoutListener()è deprecato nel livello API 16. usa removeOnGlobalLayoutListener()invece. Documentazione .
pez

errore di battitura removeOnGLobalLayoutListener dovrebbe essere removeOnGlobalLayoutListener
Theo

17

Bene, questo è quello che ho usato, abbastanza semplice, ma fa il lavoro per me. Questo codice fondamentalmente ottiene la larghezza dello schermo in intervalli e quindi si divide per 300 (o qualunque larghezza tu stia usando per il layout dell'adattatore). Quindi i telefoni più piccoli con una larghezza di 300-500 dip visualizzano solo una colonna, i tablet 2-3 colonne ecc. Semplice, senza complicazioni e senza inconvenienti, per quanto posso vedere.

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

1
Perché utilizzare la larghezza dello schermo invece della larghezza di RecyclerView? E l'hardcoding 300 è una cattiva pratica (deve essere mantenuto sincronizzato con il layout xml)
foo64

@ foo64 in xml puoi semplicemente impostare match_parent sull'elemento. Ma sì, è ancora brutto;)
Philip Giuliani

15

Ho esteso RecyclerView e ho ignorato il metodo onMeasure.

Ho impostato la larghezza di un elemento (variabile membro) il prima possibile, con un valore predefinito di 1. Questo si aggiorna anche sulla configurazione modificata. Questo ora avrà tutte le righe che possono adattarsi in verticale, orizzontale, telefono / tablet ecc.

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

12
+1 Chiu-ki Chan ha un post sul blog che delinea questo approccio e anche un progetto di esempio.
CommonsWare

7

Sto postando questo solo nel caso in cui qualcuno ottenga una larghezza di colonna strana come nel mio caso.

Non sono in grado di commentare la risposta di @s-mark a causa della mia scarsa reputazione. Ho applicato la sua soluzione , ma ho ottenuto una strana larghezza di colonna, quindi ho modificato la funzione checkColumnWidth come segue:

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

Convertendo la larghezza della colonna data in DP risolto il problema.


2

Per adattarsi alla modifica dell'orientamento sulla risposta delle s-mark , ho aggiunto un controllo sulla modifica della larghezza (larghezza da getWidth (), non larghezza della colonna).

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

2

La soluzione con voto positivo va bene, ma gestisce i valori in arrivo come pixel, il che può farti inciampare se stai codificando valori per testare e assumere dp. Il modo più semplice è probabilmente quello di inserire la larghezza della colonna in una dimensione e leggerla durante la configurazione di GridAutofitLayoutManager, che convertirà automaticamente dp nel valore di pixel corretto:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))

sì, questo mi ha buttato a capofitto. in realtà sarebbe solo accettare la risorsa stessa. Voglio dire, è come sempre quello che faremo.
Tatarize

2
  1. Imposta la larghezza fissa minima di imageView (144dp x 144dp ad esempio)
  2. Quando crei GridLayoutManager, devi sapere quante colonne saranno con una dimensione minima di 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. Dopodiché è necessario ridimensionare imageView nell'adattatore se si dispone di spazio nella colonna. È possibile inviare newImageViewSize quindi disattivare l'adattatore dall'attività in cui si calcola il conteggio di schermate e colonne:

    @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); //Установка параметров
       ...
    }

Funziona in entrambi gli orientamenti. In verticale ho 2 colonne e in orizzontale - 4 colonne. Il risultato: https://i.stack.imgur.com/WHvyD.jpg



1

Questa è la classe di s.maks con una correzione minore per quando la stessa recyclerview cambia dimensione. Ad esempio, quando android:configChanges="orientation|screenSize|keyboardHidden"gestisci tu stesso i cambiamenti di orientamento (nel manifest ), o per qualche altro motivo il recyclerview potrebbe cambiare le dimensioni senza che mColumnWidth cambi. Ho anche cambiato il valore int necessario per essere la risorsa della dimensione e ho permesso a un costruttore di nessuna risorsa di farlo da solo a setColumnWidth.

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

-1

Impostare spanCount su un numero elevato (che è il numero massimo di colonne) e impostare un SpanSizeLookup personalizzato su GridLayoutManager.

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

È un po 'brutto, ma funziona.

Penso che un manager come AutoSpanGridLayoutManager sarebbe la soluzione migliore, ma non ho trovato niente del genere.

EDIT: c'è un bug, su alcuni dispositivi aggiunge uno spazio vuoto a destra


Lo spazio a destra non è un bug. se il conteggio dello span è 5 e getSpanSizerestituisce 3, ci sarà uno spazio perché non stai riempiendo lo span.
Nicolas Tyler

-6

Ecco le parti rilevanti di un wrapper che ho utilizzato per rilevare automaticamente il conteggio degli intervalli. Lo si inizializza chiamando setGridLayoutManagercon un riferimento R.layout.my_grid_item e calcola quanti di questi possono adattarsi a ciascuna riga.

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

1
Perché downvote alla risposta accettata. dovrebbe spiegare perché downvote
androidXP
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.