RecyclerView GridLayoutManager: як автоматично виявити кількість проміжків?


110

Використання нового GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Це займає чіткий проміжок підрахунку, тому тепер стає проблемою: як ви знаєте, скільки "прольотів" поміщається в ряд? Це сітка, зрештою. Повинна бути стільки прольотів, скільки вміщається RecyclerView, виходячи з виміряної ширини.

Використовуючи стару GridView, ви просто встановите властивість "columnWidth", і вона автоматично визначить, скільки стовпців вміщається. Це в основному те, що я хочу копіювати для RecyclerView:

  • додати OnLayoutChangeListener на RecyclerView
  • у цьому зворотному дзвінку надуйте один "елемент сітки" і виміряйте його
  • spanCount = recilerViewWidth / singleItemWidth;

Це здається досить поширеною поведінкою, тож чи є простіший спосіб, який я не бачу?

Відповіді:


122

Особисто мені не подобається підклас RecyclerView для цього, тому що мені здається, що GridLayoutManager несе відповідальність за виявлення кількості прольотів. Отже, після деякого копання андроїд-коду для 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);
    }
}

Я насправді не пам'ятаю, чому я вирішив встановити кількість прольотів у onLayoutChildren, я писав цей клас деякий час тому. Але справа в тому, що нам потрібно це зробити після того, як огляд буде виміряний. щоб ми могли отримати його висоту і ширину.

EDIT 1: Виправлення помилки в коді, викликаної неправильним встановленням кількості прольотів. Дякуємо користувачеві @Elyees Abouda за повідомлення та пропозицію рішення .

EDIT 2: Деякі невеликі рефактори та виправлення кромки з ручною зміною орієнтації. Дякуємо користувачеві @tatarize за повідомлення та пропозицію рішення .


8
Це повинно бути прийнято відповідати, це LayoutManager«s робота закладати дітей поза , а не RecyclerView" s
Mewa

3
@ s.maks: Я маю на увазі, що ширина стовпця буде відрізнятися залежно від даних, переданих в адаптер. Скажіть, якщо чотири букви пропущено, то воно повинно вміщувати чотири елементи підряд, якщо пропущено 10 букв, то воно повинне вміщувати лише два елементи підряд.
Шубхам

2
Для мене при обертанні пристрою трохи змінюється розмір сітки, скорочуючись на 20 дп. Отримаєте інформацію про це? Дякую.
Олександр

3
Іноді getWidth()або getHeight()дорівнює 0, перш ніж буде створено представлення, що отримає неправильний spanCount (1, оскільки totalSpace буде <= 0). Що я додав - це ігнорування setSpanCount у цьому випадку. ( onLayoutChildrenбуде називатися знову пізніше)
Elyess Abouda

3
Існує крайовий стан, який не охоплюється. Якщо ви встановите configChanges таким чином, щоб ви обробляли обертання, а не дозволяли йому відновлювати всю активність, у вас є дивний випадок, що ширина рециркулятора переглядає, а нічого іншого не робить. Зі зміненою шириною та висотою пробіг забруднений, але mColumnWidth не змінився, тому onLayoutChildren () перериває і не перераховує зараз брудні значення. Збережіть попередню висоту та ширину та запустіть, якщо вона зміниться не нульовим способом.
Татариз

29

Я зробив це, використовуючи спостерігач з дерева перегляду, щоб отримати ширину перегляду 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();
                }
            });

2
Я використав це і отримав ArrayIndexOutOfBoundsException( at android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) під час прокрутки RecyclerView.
fikr4n

Просто додайте mLayoutManager.requestLayout () після setSpanCount (), і це працює
alvinmeimoun

2
Примітка: removeGlobalOnLayoutListener()застаріло в рівні 16 API removeOnGlobalLayoutListener(). Документація .
пез

помилка видаленняOnGLobalLayoutListener повинна бути видаленаOnGlobalLayoutListener
Theo

17

Ну, це те, що я використовував, досить базове, але роботу я робив за мене. Цей код в основному отримує ширину екрана в крапках, а потім ділиться на 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);

1
Навіщо використовувати ширину екрана замість ширини RecyclerView? І жорстке кодування 300 - це погана практика (її потрібно підтримувати синхронізувати з вашою
версією

@ foo64 в xml ви можете просто встановити match_parent на елементі. Але так, це все ще потворно;)
Філіп Джуліані

15

Я розширив 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);
        }
    }
}

12
+1 Chiu-ki Chan має публікацію в блозі, в якій викладений такий підхід та зразок проекту для нього.
CommonsWare

7

Я публікую це на випадок, якщо хтось отримає дивну ширину стовпців, як у моєму випадку.

Я не в змозі коментувати відповідь @ 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, виправлена ​​проблема.


2

Щоб змінити орієнтацію на відповідь s-позначок , я додав чек на зміну ширини (ширина від 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);
}

2

Обґрунтоване рішення чудово, але обробляє вхідні значення у вигляді пікселів, що може відключити вас, якщо ви твердо кодуєте значення для тестування та припущенні dp. Найпростіший спосіб - це, мабуть, помістити ширину стовпця в розмір і прочитати її під час налаштування GridAutofitLayoutManager, який автоматично перетворить dp у правильне значення пікселя:

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

так, це мене кинуло за петлю. насправді було б просто прийняти сам ресурс. Я маю на увазі, що як завжди, що ми будемо робити.
Татариз

2
  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



1

Це клас s.maks з незначним виправленням, коли рециркулятор сам змінює розмір. Наприклад, коли ви маєте справу зі змінами орієнтації самостійно (у маніфесті android:configChanges="orientation|screenSize|keyboardHidden") або з іншої причини, що рециркулятор може змінити розмір, не змінюючи ширину mColumnWidth. Я також змінив значення int, яке потрібно, щоб бути ресурсом розміру, і дозволив конструктору без ресурсу, а потім встановитиColumnWidth зробити це самостійно.

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

Встановіть 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, але я не знайшов нічого подібного.

EDIT: Помилка є, на деяких пристроях вона додає порожній простір праворуч


Простір справа - це не помилка. якщо кількість прольотів дорівнює 5, а getSpanSizeповертається 3, буде пробіл, оскільки ви не заповнюєте проміжок.
Ніколя Тайлер

-6

Ось відповідні частини обгортки, які я використовував для автоматичного визначення кількості проміжків. Ви ініціалізуєте його, зателефонувавши 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();
        }
    }
}

1
Чому зволікаю до прийнятої відповіді. слід пояснити, чому downvote
androidXP
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.