RecyclerView GridLayoutManager: как да откривам автоматично броя на обхватите?

Използване на новия GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Необходим е явен брой на обхвата, така че проблемът сега става: как да разберете колко "обхвата" се побират на ред? Това все пак е решетка. Трябва да има толкова участъци, колкото може да побере RecyclerView въз основа на измерената ширина.

Използвайки стария GridView, вие просто бихте задали свойството "columnWidth" и то автоматично ще открие колко колони се побират. Това е основно това, което искам да копирам за RecyclerView:

  • добавете OnLayoutChangeListener на RecyclerView
  • в това обратно извикване раздуйте единичен „елемент от мрежата“ и го измерете
  • spanCount = recyclerViewWidth / singleItemWidth;

Това изглежда като доста обичайно поведение, така че има ли по-прост начин, който не виждам?


person foo64    schedule 31.10.2014    source източник


Отговори (14)


Лично аз не обичам да подкласифицирам RecyclerView за това, защото за мен изглежда, че има отговорност на GridLayoutManager да открие броя на обхватите. Така че след известно копаене на изходния код на Android за RecyclerView и GridLayoutManager написах моя собствен разширен клас GridLayoutManager, който върши работата:

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

Всъщност не помня защо избрах да задам span count в onLayoutChildren, написах този клас преди известно време. Но въпросът е, че трябва да го направим, след като видимостта бъде измерена. така че можем да получим неговата височина и ширина.

РЕДАКТИРАНЕ 1: Коригиране на грешка в кода, причинена от неправилно задаване на броя на диапазоните. Благодарим на потребителя @Elyees Abouda за докладването и предложенията решение.

РЕДАКТИРАНЕ 2: Малко преработване и корекция на краен случай с ръчна обработка на промените в ориентацията. Благодарим на потребителя @tatarize за докладването и предложенията решение.

person s.maks    schedule 15.05.2015
comment
Това трябва да е приетият отговор, работа на LayoutManager е да разположи децата, а не на RecyclerView - person mewa; 27.08.2015
comment
Ами ако ColumnWidth не е поправено. - person Shubham; 09.12.2015
comment
@Shubham имате предвид ситуация, когато ширината на колоната трябва да се различава в зависимост от ориентацията например? - person s.maks; 10.12.2015
comment
@s.maks: Искам да кажа, че columnWidth ще се различава в зависимост от данните, предадени в адаптера. Да кажем, че ако са предадени думи от четири букви, тогава трябва да побере четири елемента в ред, ако са предадени думи от 10 букви, тогава трябва да може да побере само 2 елемента в един ред. - person Shubham; 11.12.2015
comment
@Shubham, о. Виждам. Е, мисля, че не трябва да добавяте тази функционалност към LayoutManager, защото по принцип LayoutManager не трябва да работи с действителни данни. Знае само, че трябва да оформя нещо в зависимост от моите параметри и не знае нищо за това нещо. Така че мисля, че в този случай трябва да напишете тази логика извън LayoutManager, в Activity.onCreate(), напр. Как да стане това? Може да е сложно в зависимост от данните, предадени на вашия адаптер, и действителните изисквания. Предлагам ви да създадете отделен въпрос тук за това, така че всеки да може лесно да намери отговор по-късно. - person s.maks; 11.12.2015
comment
@s.maks Имам изключение вътре в setSpanCount: java.lang.IllegalStateException: Не мога да извикам този метод, докато RecyclerView изчислява оформление или превъртане - person Oleksandr Albul; 16.03.2016
comment
@OleksandrAlbul извиквате ли ръчно setSpanCount(int) в мениджъра на оформлението? Също така откъде извиквате setLayoutManager(LayoutManager) на вашия RecyclerView? - person s.maks; 22.03.2016
comment
Проблемите в този код са следните: ако columnWidth = 300 и totalSpace = 736, тогава spanCount =2, което води до непропорционално подреждане на елементи. 2 * 300 = 600, останалите 136 пиксела не се броят, което води до неравно пълнеж - person azizbekian; 28.03.2016
comment
@azizbekian, не съм сигурен, че проблемът възниква поради този код, но не мога да кажа със сигурност, докато не видя как сте настроили вашия RecyclerView и неговите елементи. Моля, създайте въпрос на SO и публикувайте код, за да мога да ви помогна по някакъв начин. - person s.maks; 04.04.2016
comment
При мен при завъртане на устройството, той променя малко размера на решетката, свивайки се с 20dp. Имате ли информация за това? Благодаря. - person Alexandre; 07.04.2016
comment
@Alexandre същото нещо, този код работи перфектно в моите проекти, така че не мога да кажа какво точно причинява проблеми, без да гледам кода ви. - person s.maks; 08.04.2016
comment
@s.maks Актуализирах библиотеката за поддръжка до 23.3.0 и това спря да се случва. Предполагам, че е бъг от 23.0.0. Благодаря за мениджъра, хубав откъс. - person Alexandre; 08.04.2016
comment
@Alexandre Благодаря ви, радвам се да чуя, че проблемът ви е решен. - person s.maks; 08.04.2016
comment
Понякога getWidth() или getHeight() е 0, преди изгледът да бъде създаден, което ще получи грешен spanCount (1, тъй като totalSpace ще бъде ‹=0). Това, което добавих, е игнорирането на setSpanCount в този случай. (onLayoutChildren ще бъде извикан отново по-късно) - person Elyess Abouda; 20.05.2016
comment
Този код фиксира ширината за всеки елемент и след като получи span count, се фиксира завинаги. Търсех нещо като WRAP_CONTENT за всеки елемент и различен брой интервали за различен ред. - person Jimit Patel; 29.08.2016
comment
Получавам OOM грешка при super.onLayoutChildren(). Всяка помощ се оценява - person Rushi M Thakker; 17.08.2017
comment
Има крайно състояние, което не е покрито. Ако зададете configChanges така, че да управлявате ротацията, вместо да го оставите да възстанови цялата дейност, имате странния случай, че ширината на recyclerview се променя, докато нищо друго не го прави. С променена ширина и височина spancount е мръсен, но mColumnWidth не се е променил, така че onLayoutChildren() прекъсва и не преизчислява сега мръсните стойности. Запазете предишната височина и ширина и задействайте, ако се промени по ненулев начин. - person Tatarize; 15.12.2018

Постигнах това с помощта на наблюдател на дървото на изгледа, за да получа ширината на recylcerview веднъж изобразен и след това да получа фиксираните размери на моя изглед на карта от ресурси и след това да настроя броя на обхватите, след като направих изчисленията си. Това е наистина приложимо само ако елементите, които показвате, са с фиксирана ширина. Това ми помогна автоматично да попълня решетката, независимо от размера на екрана или ориентацията.

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();
                }
            });
person speedynomads    schedule 18.11.2014
comment
Използвах това и получих ArrayIndexOutOfBoundsException (в android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:361)) при превъртане на RecyclerView. - person fikr4n; 01.12.2014
comment
Просто добавете mLayoutManager.requestLayout() след setSpanCount() и работи - person alvinmeimoun; 04.12.2014
comment
Забележка: removeGlobalOnLayoutListener() е остарял в API ниво 16. Използвайте removeOnGlobalLayoutListener() вместо това. Документация. - person pez; 20.01.2015
comment
правописна грешка removeOnGLobalLayoutListener трябва да бъде removeOnGlobalLayoutListener - person Theo; 06.01.2018

Е, това е, което използвах, доста основно, но ми върши работата. Този код основно получава ширината на екрана в спадове и след това дели на 300 (или каквато и да е ширина, която използвате за оформлението на вашия адаптер). Така че по-малките телефони с ширина на падане 300-500 показват само една колона, таблетите 2-3 колони и т.н. Просто, без проблеми и без недостатъци, доколкото виждам.

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);
person Ribs    schedule 21.01.2015
comment
Защо да използвате ширината на екрана вместо ширината на RecyclerView? И твърдото кодиране 300 е лоша практика (трябва да се поддържа в синхрон с вашето xml оформление) - person foo64; 27.03.2015
comment
@foo64 в xml можете просто да зададете match_parent на елемента. Но да, все още е грозно ;) - person Philip Giuliani; 15.12.2015

Разширих RecyclerView и отмених метода onMeasure.

Зададох ширина на елемента (членна променлива) възможно най-рано, със стойност по подразбиране 1. Това също актуализира променената конфигурация. Това вече ще има толкова редове, колкото могат да се поберат в портрет, пейзаж, телефон/таблет и т.н.

@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);
        }
    }
}
person andrew_s    schedule 10.12.2014
comment
+1 Chiu-ki Chan има публикация в блог, очертаваща това подход и примерен проект също и за него. - person CommonsWare; 06.02.2015

Публикувам това само в случай, че някой получи странна ширина на колоната, както в моя случай.

Не мога да коментирам отговора на @s-marks поради моята ниска репутация. Приложих неговото решение решение, но получих някаква странна ширина на колоната, така че промених функцията checkedColumnWidth като следва:

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

Чрез преобразуване на дадената ширина на колона в DP коригира проблема.

person Ariq    schedule 28.06.2016

По-добър начин (imo) би бил да се дефинират различни интервали в (много) различни values директории и да се позволи на устройството автоматично да избере кой диапазон да използва. Например,

values/integers.xml -> span_count=3

values-w480dp/integers.xml -> span_count=4

values-w600dp/integers.xml -> span_count=5

person denvercoder9    schedule 26.09.2020

За да приспособя промяната на ориентацията на отговора на s-marks, добавих отметка на промяна на ширината (ширина от getWidth(), а не ширина на колона).

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);
}
person Andreas    schedule 15.02.2017

Гласуваното решение е добре, но обработва входящите стойности като пиксели, което може да ви препъне, ако твърдо кодирате стойности за тестване и приемате dp. Най-лесният начин вероятно е да поставите ширината на колоната в измерение и да я прочетете, когато конфигурирате GridAutofitLayoutManager, който автоматично ще преобразува dp в правилна стойност на пиксела:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
person toncek    schedule 05.05.2017
comment
да, това ме хвърли в кръг. наистина би било просто да приемете самия ресурс. Искам да кажа, че ще правим както винаги. - person Tatarize; 15.12.2018

  1. Задайте минимална фиксирана ширина на imageView (144dp x 144dp например)
  2. Когато създавате GridLayoutManager, трябва да знаете колко колони ще има с минимален размер на 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. След това трябва да преоразмерите imageView в адаптера, ако имате място в колоната. Можете да изпратите newImageViewSize, след което да инициализирате адаптера от дейността, където изчислявате броя на екрана и колоните:

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

Работи и в двете ориентации. Във вертикално имам 2 колони, а в хоризонтално - 4 колони. Резултатът: https://i.stack.imgur.com/WHvyD.jpg

person SerjantArbuz    schedule 28.05.2017

Заключавам по-горе отговорите тук

person Omid Raha    schedule 10.11.2017

Това е класът на s.maks с малка корекция за случаите, когато самият recyclerview променя размера. Като например когато вие сами се справяте с промените в ориентацията (в манифеста android:configChanges="orientation|screenSize|keyboardHidden") или по някаква друга причина recyclerview може да промени размера без mColumnWidth да се промени. Също така промених int стойността, която е необходима, за да бъде ресурсът на размера, и позволих на конструктор без ресурс, след което 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;
        }
    }
}
person Tatarize    schedule 15.12.2018

Харесвам отговора на s.maks, но открих друг ръбов случай: Ако зададете височината на RecyclerView на WRAP_CONTENT, може да се случи височината на recyclerview да бъде изчислена неправилно въз основа на остаряла стойност на spanCount. Решението, което намерих, е малка модификация на предложения метод onLayoutChildren():

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 || getOrientation() == HORIZONTAL) && (height > 0 || getOrientation() == VERTICAL) && (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);
        if (getSpanCount() != spanCount) {
            setSpanCount(spanCount);
        }
        isColumnWidthChanged = false;
    }
    lastWidth = width;
    lastHeight = height;
    super.onLayoutChildren(recycler, state);
}
person Lars Brinkmann    schedule 17.07.2021

Задайте spanCount на голямо число (което е максималния брой колони) и задайте персонализиран SpanSizeLookup на GridLayoutManager.

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

Малко е грозно, но работи.

Мисля, че мениджър като AutoSpanGridLayoutManager би бил най-доброто решение, но не намерих нищо подобно.

РЕДАКТИРАНЕ: Има грешка, на някое устройство се добавя празно място вдясно

person alvinmeimoun    schedule 12.11.2014
comment
Мястото вдясно не е грешка. ако броят на интервалите е 5 и getSpanSize върне 3, ще има интервал, защото не запълвате диапазона. - person Nicolas Tyler; 27.02.2015

Ето съответните части от обвивка, която съм използвал за автоматично откриване на броя на интервалите. Вие го инициализирате, като извикате setGridLayoutManager с препратка R.layout.my_grid_item и той открива колко от тях могат да се поберат на всеки ред.

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();
        }
    }
}
person foo64    schedule 27.03.2015
comment
Защо да гласувате против приетия отговор. трябва да обясни защо гласува против - person androidXP; 03.02.2018