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


С помощью нового GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

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

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

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

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

11 90

11 ответов:

лично мне не нравится подкласс RecyclerView для этого, потому что для меня кажется, что есть gridlayoutmanager ответственность за обнаружение span count. Поэтому после некоторого исходного кода android, копающего для RecyclerView и GridLayoutManager, я написал свой собственный класс extended GridLayoutManager, который выполняет эту работу:

public class GridAutofitLayoutManager extends GridLayoutManager
{
    private int mColumnWidth;
    private boolean mColumnWidthChanged = true;

    public GridAutofitLayoutManager(Context context, int columnWidth)
    {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    public GridAutofitLayoutManager(Context context, int columnWidth, int orientation, boolean reverseLayout)
    {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1, orientation, reverseLayout);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    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());
        }
        return columnWidth;
    }

    public void setColumnWidth(int newColumnWidth)
    {
        if (newColumnWidth > 0 && newColumnWidth != mColumnWidth)
        {
            mColumnWidth = newColumnWidth;
            mColumnWidthChanged = true;
        }
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
    {
        int width = getWidth();
        int height = getHeight();
        if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0)
        {
            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;
        }
        super.onLayoutChildren(recycler, state);
    }
}

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

EDIT: Исправлена ошибка в коде, вызванная неправильной установкой счетчика промежутков. Спасибо пользователь @Elyees Abouda для отчетности и предлагая решение.

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

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

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

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

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

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

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

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

Я вывод выше ответы здесь

  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

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

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