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

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

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

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

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

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

Ответ 1

Лично я не люблю создавать для этого подкласс 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 за сообщение и предложение решения.

Ответ 2

Я выполнил это с помощью наблюдателя дерева представлений, чтобы получить ширину 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();
                }
            });

Ответ 3

Хорошо, это то, что я использовал, довольно простой, но выполняет работу для меня. Этот код в основном получает ширину экрана в провалах, а затем делит на 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);

Ответ 4

Я расширил 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);
        }
    }
}

Ответ 5

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

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

Ответ 6

Чтобы изменить ориентацию на 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);
}

Ответ 7

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

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

Ответ 8

I заключение выше отвечает здесь

Ответ 9

  • Установите минимальную фиксированную ширину imageView (например, 144dp x 144dp)
  • Когда вы создаете GridLayoutManager, вам нужно знать, сколько столбцов будет иметь минимальный размер изображения:

    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);
    
  • После этого вам нужно изменить размер изображения в адаптере, если у вас есть место в столбце. Вы можете отправить 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

Ответ 10

Это класс 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;
        }
    }
}

Ответ 11

Установите 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: есть ошибка, на каком-то устройстве он добавляет пустое пространство вправо

Ответ 12

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