RecyclerView GridLayoutManager: jak automatycznie wykryć liczbę zakresów?

110

Korzystanie z nowego GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Wymaga to wyraźnej liczby rozpiętości, więc problem teraz wygląda następująco: skąd wiesz, ile „rozpiętości” mieści się w wierszu? W końcu to jest siatka. Powinno być tyle rozpiętości, ile może zmieścić RecyclerView, na podstawie zmierzonej szerokości.

Używając starej GridView, po prostu ustawisz właściwość „columnWidth” i automatycznie wykryje, ile kolumn pasuje. To jest w zasadzie to, co chcę powielić dla RecyclerView:

  • dodaj OnLayoutChangeListener na RecyclerView
  • w tym wywołaniu zwrotnym napompuj pojedynczy „element siatki” i zmierz go
  • spanCount = recyklerViewWidth / singleItemWidth;

Wydaje się, że jest to dość powszechne zachowanie, więc czy istnieje prostszy sposób, którego nie widzę?

foo64
źródło

Odpowiedzi:

122

Osobiście nie lubię podklasy RecyclerView w tym celu, ponieważ wydaje mi się, że GridLayoutManager jest odpowiedzialny za wykrywanie liczby rozpiętości. Więc po pewnym kopaniu kodu źródłowego Androida dla RecyclerView i GridLayoutManager napisałem własną rozszerzoną klasę GridLayoutManager, która wykonuje zadanie:

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

Właściwie nie pamiętam, dlaczego zdecydowałem się ustawić licznik rozpiętości w onLayoutChildren, napisałem tę klasę jakiś czas temu. Ale chodzi o to, że musimy to zrobić po zmierzeniu widoku. abyśmy mogli uzyskać jego wysokość i szerokość.

EDYCJA 1: Napraw błąd w kodzie, który powodował nieprawidłowe ustawienie liczby rozpiętości. Dziękuję użytkownikowi @Elyees Abouda za zgłoszenie i zasugerowanie rozwiązania .

EDYCJA 2: Trochę małych refaktoryzacji i napraw krawędziowych z ręczną obsługą zmian orientacji. Dziękuję użytkownikowi @tatarize za zgłoszenie i zasugerowanie rozwiązania .

s.maks
źródło
8
To powinno być przyjętym odpowiedź, to LayoutManagerjest zadanie, aby stworzyć dzieciom out i nie RecyclerView's
mewa
3
@ s.maks: mam na myśli columnWidth będzie się różnić w zależności od danych przesyłanych do adaptera. Powiedzmy, że jeśli przekazywane są słowa czteroliterowe, to powinno zawierać cztery elementy w rzędzie, jeśli przekazywane są słowa 10-literowe, to powinno być w stanie pomieścić tylko 2 elementy z rzędu.
Shubham,
2
U mnie obracając urządzenie zmienia trochę rozmiar siatki, zmniejszając się o 20dp. Masz jakieś informacje na ten temat? Dzięki.
Alexandre
3
Czasami wartość getWidth()lub getHeight()wynosi 0 przed utworzeniem widoku, co spowoduje wyświetlenie nieprawidłowego spanCount (1, ponieważ totalSpace będzie wynosić <= 0). Dodałem w tym przypadku ignorowanie setSpanCount. ( onLayoutChildrenzostanie ponownie wezwany później)
Elyess Abouda
3
Istnieje stan krawędzi, który nie jest objęty. Jeśli ustawisz configChanges w taki sposób, aby obsłużyć rotację, zamiast pozwolić jej odbudować całą aktywność, masz dziwny przypadek, że szerokość widoku recyklingu zmienia się, podczas gdy nic innego się nie zmienia. Ze zmienioną szerokością i wysokością spancount jest brudny, ale mColumnWidth się nie zmienił, więc onLayoutChildren () przerywa i nie oblicza ponownie brudnych wartości. Zapisz poprzednią wysokość i szerokość i wywołaj, jeśli zmieni się w sposób niezerowy.
Tatarize
29

Dokonałem tego za pomocą obserwatora drzewa widoków, aby uzyskać szerokość widoku recylcerview po wyrenderowaniu, a następnie uzyskać stałe wymiary widoku mojej karty z zasobów, a następnie ustawić liczbę rozpiętości po wykonaniu moich obliczeń. Ma to naprawdę zastosowanie tylko wtedy, gdy wyświetlane elementy mają stałą szerokość. Pomogło mi to automatycznie wypełnić siatkę niezależnie od rozmiaru ekranu lub orientacji.

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
źródło
2
Użyłem tego i otrzymałem ArrayIndexOutOfBoundsException( na android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) podczas przewijania plikuRecyclerView .
fikr4n
Po prostu dodaj mLayoutManager.requestLayout () po setSpanCount () i działa
alvinmeimoun
2
Uwaga: removeGlobalOnLayoutListener()jest nieaktualne w przypadku użycia interfejsu API 16. poziomu removeOnGlobalLayoutListener(). Dokumentacja .
pez
literówka removeOnGLobalLayoutListener powinna zostać usuniętaOnGlobalLayoutListener
Theo
17

Cóż, to jest to, czego użyłem, dość podstawowe, ale wykonuje pracę za mnie. Ten kod w zasadzie pobiera szerokość ekranu w spadkach, a następnie dzieli przez 300 (lub jakąkolwiek szerokość, której używasz dla układu karty). Tak więc mniejsze telefony o szerokości zanurzenia 300-500 wyświetlają tylko jedną kolumnę, tablety 2-3 kolumny itd. Prosty, bezproblemowy i bez wad, o ile widzę.

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);
Żebra
źródło
1
Po co używać szerokości ekranu zamiast szerokości RecyclerView? A
twarde kodowanie
@ foo64 w xml możesz po prostu ustawić match_parent na elemencie. Ale tak, nadal jest brzydki;)
Philip Giuliani
15

Rozszerzyłem RecyclerView i zastąpiłem metodę onMeasure.

Ustawiam szerokość elementu (zmienną składową) tak wcześnie, jak to możliwe, z domyślną wartością 1. To również aktualizuje się po zmianie konfiguracji. Będzie miał teraz tyle wierszy, ile zmieści się w pionie, poziomie, telefonie / tablecie itp.

@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
źródło
12
+1 Chiu-ki Chan ma post na blogu opisujący to podejście, a także przykładowy projekt .
CommonsWare
7

Publikuję to na wypadek, gdyby ktoś miał dziwną szerokość kolumny, jak w moim przypadku.

Nie mogę skomentować odpowiedzi @ s-marks z powodu mojej złej reputacji. Zastosowałem jego rozwiązanie , ale otrzymałem dziwną szerokość kolumny, więc zmodyfikowałem funkcję CheckColumnWidth w następujący sposób:

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

Konwersja podanej szerokości kolumny na DP rozwiązała problem.

Ariq
źródło
2

Aby uwzględnić zmianę orientacji w odpowiedzi s- mark, dodałem kontrolę zmiany szerokości (szerokość z getWidth (), a nie szerokość kolumny).

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
źródło
2

Uznane rozwiązanie jest w porządku, ale traktuje przychodzące wartości jako piksele, co może spowodować błąd, jeśli zakodujesz na stałe wartości do testowania i przyjmujesz dp. Najłatwiejszym sposobem jest prawdopodobnie umieszczenie szerokości kolumny w wymiarze i odczytanie go podczas konfigurowania GridAutofitLayoutManager, który automatycznie przekonwertuje dp na prawidłową wartość piksela:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
toncek
źródło
tak, to rzuciło mnie na pętlę. naprawdę wystarczyłoby zaakceptować sam zasób. Mam na myśli to, że tak jak zawsze będziemy robić.
Tatarize
2
  1. Ustaw minimalną stałą szerokość imageView (na przykład 144dp x 144dp)
  2. Kiedy tworzysz GridLayoutManager, musisz wiedzieć, ile kolumn będzie przy minimalnym rozmiarze 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. Następnie musisz zmienić rozmiar imageView w adapterze, jeśli masz miejsce w kolumnie. Możesz wysłać newImageViewSize, a następnie zainizilizować adapter z aktywności, w której obliczasz liczbę ekranów i kolumn:

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

Działa w obu orientacjach. W pionie mam 2 kolumny, aw poziomie - 4 kolumny. Wynik: https://i.stack.imgur.com/WHvyD.jpg

Serjant.arbuz
źródło
1

To jest klasa s.maks z drobną poprawką dotyczącą zmiany rozmiaru samego widoku recyklingu. Na przykład, gdy sam zajmujesz się zmianami orientacji (w manifeście android:configChanges="orientation|screenSize|keyboardHidden") lub z innego powodu widok recyklingu może zmienić rozmiar bez zmiany mColumnWidth. Zmieniłem również wartość int, która ma być zasobem rozmiaru, i pozwoliłem konstruktorowi bez zasobu, a następnie setColumnWidth zrobić to sam.

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;
        }
    }
}
Tatarize
źródło
-1

Ustaw spanCount na dużą liczbę (która jest maksymalną liczbą kolumn) i ustaw niestandardowy SpanSizeLookup na GridLayoutManager.

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

To trochę brzydkie, ale działa.

Myślę, że menedżer taki jak AutoSpanGridLayoutManager byłby najlepszym rozwiązaniem, ale nie znalazłem czegoś takiego.

EDYCJA: Wystąpił błąd, na niektórych urządzeniach dodaje puste miejsce po prawej stronie

alvinmeimoun
źródło
Przestrzeń po prawej stronie nie jest błędem. jeśli liczba zakresów wynosi 5 i getSpanSizezwraca 3, będzie spacja, ponieważ nie wypełniasz zakresu.
Nicolas Tyler
-6

Oto odpowiednie części opakowania, którego używałem do automatycznego wykrywania liczby rozpiętości. Zainicjować go poprzez wywołanie setGridLayoutManagerz R.layout.my_grid_item odniesienia, i domyśla się, ile osób zmieści się w każdym wierszu.

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
źródło
1
Po co głosować przeciw zaakceptowanej odpowiedzi. Powinien wyjaśnić, dlaczego głosowanie przeciw
androidXP