RecyclerView GridLayoutManager: Wie erkennt man automatisch die Anzahl der Spannen?


110

Verwenden des neuen GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Da eine explizite Anzahl von Bereichen erforderlich ist, stellt sich nun das Problem: Woher wissen Sie, wie viele "Bereiche" pro Zeile passen? Immerhin ist dies ein Raster. Es sollten so viele Bereiche vorhanden sein, wie der RecyclerView basierend auf der gemessenen Breite passen kann.

Mit der alten GridViewwürden Sie einfach die Eigenschaft "columnWidth" festlegen und automatisch erkennen, wie viele Spalten passen. Dies ist im Grunde das, was ich für RecyclerView replizieren möchte:

  • Fügen Sie OnLayoutChangeListener auf dem hinzu RecyclerView
  • Blasen Sie in diesem Rückruf ein einzelnes 'Gitterelement' auf und messen Sie es
  • spanCount = recyclerViewWidth / singleItemWidth;

Dies scheint ein ziemlich häufiges Verhalten zu sein. Gibt es also einen einfacheren Weg, den ich nicht sehe?

Antworten:


122

Persönlich möchte ich RecyclerView dafür nicht unterordnen, da es für mich so scheint, als ob GridLayoutManager dafür verantwortlich ist, die Anzahl der Bereiche zu ermitteln. Nachdem ich einige Android-Quellcodes für RecyclerView und GridLayoutManager ausgegraben hatte, schrieb ich meinen eigenen klassenerweiterten GridLayoutManager, der die Aufgabe erledigt:

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

Ich erinnere mich nicht wirklich, warum ich mich entschieden habe, die Anzahl der Spannen in onLayoutChildren festzulegen. Ich habe diese Klasse vor einiger Zeit geschrieben. Der Punkt ist jedoch, dass wir dies tun müssen, nachdem die Ansicht gemessen wurde. so können wir seine Höhe und Breite erhalten.

BEARBEITEN 1: Fehler im Code behoben, der dazu führte, dass die Anzahl der Bereiche falsch eingestellt wurde. Vielen Dank an Benutzer @Elyees Abouda für die Berichterstattung und den Lösungsvorschlag .

EDIT 2: Einige kleine Refactoring- und Fixkantengehäuse mit manueller Ausrichtung ändern die Handhabung. Vielen Dank an Benutzer @tatarize für die Berichterstellung und den Lösungsvorschlag .


8
Dies sollte die akzeptierte Antwort, es ist LayoutManager's Aufgabe , die Kinder und nicht zu legen RecyclerViewist
mewa

3
@ s.maks: Ich meine, columnWidth würde sich je nach den an den Adapter übergebenen Daten unterscheiden. Angenommen, wenn Wörter mit vier Buchstaben übergeben werden, sollten vier Elemente in einer Reihe untergebracht werden. Wenn Wörter mit 10 Buchstaben übergeben werden, sollten nur zwei Elemente in einer Reihe untergebracht werden können.
Shubham

2
Wenn ich das Gerät drehe, ändert sich für mich die Größe des Rasters ein wenig und schrumpft um 20 dp. Hast du irgendwelche Infos dazu? Vielen Dank.
Alexandre

3
Manchmal getWidth()oder getHeight()ist 0, bevor die Ansicht erstellt wird, wodurch ein falscher spanCount angezeigt wird (1, da totalSpace <= 0 ist). Was ich hinzugefügt habe, ist in diesem Fall setSpanCount zu ignorieren. ( onLayoutChildrenwird später noch einmal genannt)
Elyess Abouda

3
Es gibt eine Randbedingung, die nicht abgedeckt ist. Wenn Sie die configChanges so einstellen, dass Sie die Rotation handhaben, anstatt die gesamte Aktivität neu erstellen zu lassen, ändert sich gelegentlich die Breite der Recycling-Ansicht, während nichts anderes der Fall ist. Bei geänderter Breite und Höhe ist der Spancount verschmutzt, aber mColumnWidth hat sich nicht geändert, sodass onLayoutChildren () die jetzt verschmutzten Werte abbricht und nicht neu berechnet. Speichern Sie die vorherige Höhe und Breite und lösen Sie sie aus, wenn sie sich ungleich Null ändert.
Tatarize

29

Ich habe dies mit einem View Tree Observer erreicht, um die Breite der einmal gerenderten Recylcerview zu ermitteln, die festen Abmessungen meiner Kartenansicht aus den Ressourcen abzurufen und nach den Berechnungen die Anzahl der Bereiche festzulegen. Dies ist nur dann wirklich anwendbar, wenn die angezeigten Elemente eine feste Breite haben. Dadurch konnte ich das Raster unabhängig von Bildschirmgröße oder -ausrichtung automatisch ausfüllen.

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
Ich habe dies verwendet und ArrayIndexOutOfBoundsException( unter android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) beim Scrollen der RecyclerView.
fikr4n

Fügen Sie einfach mLayoutManager.requestLayout () nach setSpanCount () hinzu und es funktioniert
alvinmeimoun

2
Hinweis: removeGlobalOnLayoutListener()ist in API-Level 16 veraltet. Verwenden Sie removeOnGlobalLayoutListener()stattdessen. Dokumentation .
Pez

Tippfehler removeOnGLobalLayoutListener sollte removeOnGlobalLayoutListener sein
Theo

17

Nun, das ist es, was ich verwendet habe, ziemlich einfach, aber ich erledige die Arbeit für mich. Dieser Code ermittelt die Bildschirmbreite im Grunde genommen in Dips und dividiert sie durch 300 (oder die Breite, die Sie für das Layout Ihres Adapters verwenden). So zeigen kleinere Telefone mit einer Eintauchbreite von 300-500 nur eine Spalte, Tablets 2-3 Spalten usw. an. Einfach, unkompliziert und ohne Nachteile, soweit ich sehen kann.

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
Warum sollte die Bildschirmbreite anstelle der Breite von RecyclerView verwendet werden? Und Hardcoding 300 ist eine schlechte Praxis (es muss mit Ihrem XML-Layout synchronisiert werden)
foo64

@ foo64 in der XML können Sie einfach match_parent für das Element festlegen. Aber ja, es ist immer noch hässlich;)
Philip Giuliani

15

Ich habe die RecyclerView erweitert und die onMeasure-Methode überschrieben.

Ich habe so früh wie möglich eine Elementbreite (Elementvariable) mit dem Standardwert 1 festgelegt. Dies wird auch bei geänderter Konfiguration aktualisiert. Dies hat jetzt so viele Zeilen, wie in Hochformat, Querformat, Telefon / Tablet usw. passen.

@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 hat einen Blog-Beitrag , der diesen Ansatz beschreibt, und ein Beispielprojekt dafür.
CommonsWare

7

Ich poste dies nur für den Fall, dass jemand eine seltsame Spaltenbreite wie in meinem Fall bekommt.

Ich kann die Antwort von @ s-Marks aufgrund meines schlechten Rufs nicht kommentieren . Ich habe seine Lösungslösung angewendet , aber ich habe eine seltsame Spaltenbreite erhalten, daher habe ich die Funktion checkedColumnWidth wie folgt geändert :

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

Durch Konvertieren der angegebenen Spaltenbreite in DP wurde das Problem behoben.


2

Um die Orientierungsänderung in der Antwort von s- marken zu berücksichtigen, habe ich eine Überprüfung der Breitenänderung hinzugefügt (Breite von getWidth (), nicht Spaltenbreite).

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

Die Upvoted-Lösung ist in Ordnung, behandelt die eingehenden Werte jedoch als Pixel, was Sie auslösen kann, wenn Sie Werte zum Testen und zur Annahme von dp fest codieren. Am einfachsten ist es wahrscheinlich, die Spaltenbreite in eine Dimension zu setzen und sie bei der Konfiguration des GridAutofitLayoutManager zu lesen, der dp automatisch in den richtigen Pixelwert konvertiert:

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

Ja, das hat mich für eine Schleife geworfen. wirklich wäre es nur die Ressource selbst zu akzeptieren. Ich meine, das ist wie immer das, was wir tun werden.
Tatarize

2
  1. Stellen Sie die minimale feste Breite von imageView ein (z. B. 144 dp x 144 dp).
  2. Wenn Sie GridLayoutManager erstellen, müssen Sie wissen, wie viele Spalten bei minimaler Größe von imageView vorhanden sind:

    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. Danach müssen Sie die Größe von imageView im Adapter ändern, wenn in der Spalte Platz vorhanden ist. Sie können newImageViewSize senden und dann den Adapter von der Aktivität dort deaktivieren, in der Sie die Anzahl der Bildschirme und Spalten berechnen:

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

Es funktioniert in beiden Ausrichtungen. In der Vertikalen habe ich 2 Spalten und in der Horizontalen - 4 Spalten. Das Ergebnis: https://i.stack.imgur.com/WHvyD.jpg



1

Dies ist die Klasse von s.maks mit einer geringfügigen Korrektur für den Fall, dass sich die Größe der Recyclingansicht selbst ändert. Wenn Sie sich beispielsweise selbst (im Manifest android:configChanges="orientation|screenSize|keyboardHidden") mit den Änderungen der Ausrichtung befassen , oder aus einem anderen Grund, kann sich die Größe der Recyclingansicht ändern, ohne dass sich die mColumnWidth ändert. Ich habe auch den int-Wert geändert, der als Ressource der Größe benötigt wird, und einem Konstruktor ohne Ressource erlaubt, dann setColumnWidth, dies selbst zu tun.

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

Setzen Sie spanCount auf eine große Zahl (dies ist die maximale Anzahl von Spalten) und setzen Sie ein benutzerdefiniertes SpanSizeLookup auf den GridLayoutManager.

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

Es ist ein bisschen hässlich, aber es funktioniert.

Ich denke, ein Manager wie AutoSpanGridLayoutManager wäre die beste Lösung, aber ich habe so etwas nicht gefunden.

BEARBEITEN: Es gibt einen Fehler. Auf einigen Geräten wird rechts ein Leerzeichen hinzugefügt


Das Feld rechts ist kein Fehler. Wenn die Anzahl der Bereiche 5 beträgt und getSpanSize3 zurückgibt, wird ein Leerzeichen angezeigt, da Sie den Bereich nicht ausfüllen.
Nicolas Tyler

-6

Hier sind die relevanten Teile eines Wrappers, mit denen ich die Anzahl der Bereiche automatisch erkannt habe. Sie initialisieren es, indem Sie setGridLayoutManagermit einer R.layout.my_grid_item- Referenz aufrufen , und es wird ermittelt, wie viele davon in jede Zeile passen.

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
Warum auf akzeptierte Antwort abstimmen? sollte erklären, warum downvote
androidXP
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.