RecyclerView GridLayoutManager: как автоматически определять количество промежутков?

111

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

Требуется явное количество промежутков, поэтому теперь возникает проблема: как узнать, сколько «промежутков» умещается в одной строке? В конце концов, это сетка. Должно быть столько интервалов, сколько может поместиться RecyclerView, в зависимости от измеренной ширины.

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

  • добавить OnLayoutChangeListener в RecyclerView
  • в этом обратном вызове надуйте один «элемент сетки» и измерьте его
  • spanCount = recyclerViewWidth / singleItemWidth;

Это кажется довольно распространенным поведением, так что есть более простой способ, который я не вижу?

foo64
источник

Ответы:

123

Лично мне не нравится создавать подкласс 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);
    }
}

На самом деле я не помню, почему я решил установить счетчик промежутков в onLayoutChildren, я написал этот класс некоторое время назад. Но дело в том, что мы должны сделать это после того, как будет измерена точка зрения. так что мы можем получить его высоту и ширину.

РЕДАКТИРОВАТЬ 1: Исправить ошибку в коде, вызвавшую неправильную установку счетчика диапазона. Благодарим пользователя @Elyees Abouda за сообщение и предложение решения .

РЕДАКТИРОВАТЬ 2: Небольшой рефакторинг и исправление краевого случая с ручной обработкой изменений ориентации. Спасибо пользователю @tatarize за сообщение и предложение решения .

s.maks
источник
8
Это должно быть принято отвечать, это LayoutManager«s работа закладывать детей вне , а не RecyclerView» s
Mewa
3
@ s.maks: я имею в виду, что ширина столбца будет отличаться в зависимости от данных, передаваемых в адаптер. Скажем, если переданы слова из четырех букв, тогда он должен вместить четыре элемента в ряд, если переданы слова из 10 букв, тогда он должен быть в состоянии разместить только 2 элемента в строке.
Шубхам
2
По мне, при повороте устройства немного меняется размер сетки, уменьшаясь на 20dp. Есть информация об этом? Спасибо.
Александр
3
Иногда getWidth()или getHeight()равно 0 до создания представления, которое будет иметь неправильный spanCount (1, поскольку totalSpace будет <= 0). В этом случае я добавил игнорирование setSpanCount. ( onLayoutChildrenбудет называться позже)
Элис Абуда
3
Есть крайнее состояние, которое не покрывается. Если вы установите configChanges так, что вы обрабатываете вращение, а не позволяете ему перестраивать всю активность, у вас будет странный случай, когда ширина recyclerview изменится, в то время как больше ничего не изменится. При изменении ширины и высоты spancount становится грязным, но mColumnWidth не изменилась, поэтому onLayoutChildren () прерывается и не пересчитывает теперь грязные значения. Сохраните предыдущую высоту и ширину и активируйте, если они изменятся ненулевым образом.
Tatarize
29

Я выполнил это с помощью наблюдателя дерева представлений, чтобы получить ширину повторного просмотра после рендеринга, а затем получить фиксированные размеры представления моей карты из ресурсов, а затем установить счетчик диапазона после выполнения моих расчетов. Это действительно применимо, только если отображаемые элементы имеют фиксированную ширину. Это помогло мне автоматически заполнить сетку независимо от размера или ориентации экрана.

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( на android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) при прокрутке файла RecyclerView.
fikr4n 01
Просто добавьте mLayoutManager.requestLayout () после setSpanCount (), и он заработает
alvinmeimoun
2
Примечание: removeGlobalOnLayoutListener()не рекомендуется на уровне API 16. используйте removeOnGlobalLayoutListener()вместо него. Документация .
pez
опечатка removeOnGLobalLayoutListener должен быть removeOnGlobalLayoutListener
Тео
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 - это плохая практика (его нужно синхронизировать с вашим макетом xml)
foo64
@ 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);
        }
    }
}
andrew_s
источник
12
+1 У Чиу-ки Чана есть запись в блоге, в которой описывается этот подход, а также пример проекта для него.
CommonsWare
7

Я публикую это на всякий случай, если кто-то получит странную ширину столбца, как в моем случае.

Я не могу комментировать ответ @ s-mark из-за моей низкой репутации. Я применил свое решение решения , но я получил некоторую странную ширину столбца, поэтому я изменил 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 устранена проблема.

Ariq
источник
2

Чтобы учесть изменение ориентации в ответе s- mark, я добавил проверку изменения ширины (ширину из 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))
Тончек
источник
да, это меня зациклило. на самом деле было бы просто принять сам ресурс. Я имею в виду, что мы будем делать это всегда.
Tatarize
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, а затем inisilize адаптер из активности, где вы вычисляете количество экранов и столбцов:

    @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

Serjant.arbuz
источник
1

Это класс 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;
        }
    }
}
Tatarize
источник
-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 был бы лучшим решением, но я не нашел ничего подобного.

РЕДАКТИРОВАТЬ: есть ошибка, на каком-то устройстве он добавляет пустое пространство справа

Alvinmeimoun
источник
Пространство справа не является ошибкой. если счетчик диапазона равен 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();
        }
    }
}
foo64
источник
1
Почему проголосовали против принятого ответа. должен объяснить, почему голосование против
androidXP 03