RecyclerView GridLayoutManager:如何自动检测跨度计数?


110

使用新的GridLayoutManager:https : //developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

它需要一个显式的跨度计数,所以现在的问题就变成了:您如何知道每行有多少个“跨度”?毕竟,这是一个网格。根据测量的宽度,应该有尽可能多的RecyclerView跨度。

使用old GridView,您只需设置“ columnWidth”属性,它将自动检测可容纳多少列。这基本上是我要为RecyclerView复制的内容:

  • 在上添加OnLayoutChangeListener RecyclerView
  • 在此回调中,为单个“网格项”充气并对其进行度量
  • spanCount = recyclerViewWidth / singleItemWidth;

这似乎是很常见的行为,所以有没有我看不到的更简单方法?

Answers:


122

我个人不喜欢为此归类RecyclerView,因为对我来说,似乎GridLayoutManager负责检测跨度计数。因此,在为RecyclerView和GridLayoutManager挖掘了一些Android源代码之后,我编写了自己的扩展类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提供报告和建议解决方案


8
这应该是公认的答案,它LayoutManager的工作奠定了孩子出去,而不是RecyclerView
MEWA

3
@ s.maks:我的意思是columnWidth会根据传递到适配器的数据而有所不同。假设如果传递了四个字母单词,那么它应该连续容纳四个项目;如果传递十个字母单词,那么它应该只能连续容纳两个项目。
Shubham

2
对我来说,旋转设备时,它会稍微改变网格的大小,缩小20dp。有关于此的任何信息吗?谢谢。
Alexandre

3
在创建视图之前,有时getWidth()getHeight()为0,这将获得错误的spanCount(因为totalSpace将<= 0,所以为1)。在这种情况下,我添加的内容将忽略setSpanCount。(onLayoutChildren稍后会再次致电)
Elyess Abouda

3
有一个没有覆盖的边缘条件。如果您将configChanges设置为可以处理旋转而不是让其重建整个活动,则可能出现recyclerview的宽度发生变化而其他没有改变的奇怪情况。更改宽度和高度后,spancount变脏,但是mColumnWidth不变,因此onLayoutChildren()中止并且不重新计算现在的变脏值。保存先前的高度和宽度,并以非零的方式触发。
12/15

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位于android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:361)RecyclerView
fikr4n 2014年

只需在setSpanCount()之后添加mLayoutManager.requestLayout()即可使用
alvinmeimoun 2014年

2
注意:removeGlobalOnLayoutListener()在API级别16中已弃用。请removeOnGlobalLayoutListener()改用。文件资料
pez

错字removeOnGLobalLayoutListener应该为removeOnGlobalLayoutListener
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是不好的做法(必须与您的xml布局保持同步)
foo64 2015年

您可以在xml中使用@ foo64在项目上设置match_parent。但是,是的,它仍然很难看;)
Philip Giuliani

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,2015年

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

2

赞成的解决方案很好,但是将输入值作为像素处理,如果您要对值进行硬编码以进行测试并假设使用dp,这会使您不满意。最简单的方法可能是在配置GridAutofitLayoutManager时将列宽放入维度中并读取,这将自动将dp转换为正确的像素值:

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

是的,那确实让我陷入困境。实际上,它只是接受资源本身。我的意思是就像我们将一直做的一样。
12/15

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的课程,它对recyclerview本身更改大小时有较小的修正。例如,当您处理方向更改时(在清单中android:configChanges="orientation|screenSize|keyboardHidden"),或者其他一些原因,如果不更改mColumnWidth,则recyclerview可能会更改大小。我还更改了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;
        }
    }
}

-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这样的管理器将是最好的解决方案,但我没有找到类似的东西。

编辑:有一个错误,在某些设备上,它在右边添加了空白


右边的空间不是错误。如果范围计数为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
为什么要拒​​绝接受答案。应该解释为什么下
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.