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

foo*_*o64 101 android android-gridlayout gridlayoutmanager android-recyclerview

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

它需要一个明确的跨度计数,所以问题现在变成:你怎么知道每行有多少"跨度"?毕竟这是一个网格.根据测量的宽度,RecyclerView可以容纳多个跨距.

使用旧的GridView,您只需设置"columnWidth"属性,它将自动检测适合的列数.这基本上是我想要为RecyclerView复制的:

  • 在上添加OnLayoutChangeListener RecyclerView
  • 在此回调中,膨胀单个"网格项"并进行测量
  • spanCount = recyclerViewWidth/singleItemWidth;

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

s.m*_*aks 115

Personaly我不喜欢为此子类化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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我实际上不记得为什么我选择在onLayoutChildren中设置跨度计数,我不久前写了这个类.但重点是我们需要在观察后进行测量.所以我们可以得到它的高度和宽度.

编辑:修复导致错误设置跨度计数的代码错误.感谢用户@Elyees Abouda报告和建议解决方案.

  • 这应该是公认的答案,它是`LayoutManager`的工作,让孩子们出来而不是`RecyclerView`的 (8认同)
  • @ s.maks:我的意思是columnWidth会根据传入适配器的数据而有所不同.假设如果传递了四个字母单词,那么如果传递10个字母单词则它应该连续容纳四个项目然后它应该能够容纳连续的两个项目. (3认同)
  • 有时`getWidth()`或`getHeight()`在创建视图之前为0,这将得到一个错误的spanCount(1,因为totalSpace将<= 0).我添加的是在这种情况下忽略setSpanCount.(`onLayoutChildren`将在稍后再次调用) (3认同)
  • 有一个没有覆盖的边缘条件。如果将configChanges设置为可以处理旋转而不是让其重建整个活动,则可能出现recyclerview的宽度发生变化而其他没有改变的情况。更改宽度和高度后,spancount变脏,但是mColumnWidth不变,因此onLayoutChildren()中止并且不重新计算现在的变脏值。保存先前的高度和宽度,并以非零的方式触发。 (3认同)
  • 此代码中的问题如下:如果columnWidth = 300,并且totalSpace = 736,则spanCount = 2,这会导致项目不按比例布局。2 * 300 = 600,其余136个像素没有被计算在内,这导致[不等于填充](http://i.imgur.com/ACwum3c.png) (2认同)
  • 对我来说,当旋转设备时,它会改变网格的大小,缩小20dp.了解更多信息?谢谢. (2认同)

spe*_*ads 28

我使用视图树观察器完成了这一点,一旦渲染就获得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();
                }
            });
Run Code Online (Sandbox Code Playgroud)

  • 我在滚动"RecyclerView"时使用了这个并得到了`ArrayIndexOutOfBoundsException`(*在android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:361)*). (2认同)
  • 注:`removeGlobalOnLayoutListener()`API等级16的使用被废弃`removeOnGlobalLayoutListener()`代替.[文档](http://developer.android.com/reference/android/view/ViewTreeObserver.html#removeGlobalOnLayoutListener(android.view.ViewTreeObserver.OnGlobalLayoutListener)). (2认同)

小智 15

嗯,这是我用过的,相当基本的,但是为我完成了工作.此代码基本上以dips为单位获取屏幕宽度,然后除以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);
Run Code Online (Sandbox Code Playgroud)

  • 为什么使用屏幕宽度而不是 RecyclerView 的宽度?硬编码 300 是不好的做法(它需要与您的 xml 布局保持同步) (2认同)

小智 13

我扩展了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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • +1 Chiu-ki Chan有[博客文章](http://blog.sqisland.com/2014/12/recyclerview-autofit-grid.html)概述了这种方法和[示例项目](https:// github .com/chiuki/android-recyclerview)也是如此. (12认同)

Ari*_*riq 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;
}
Run Code Online (Sandbox Code Playgroud)

通过将给定的列宽转换为DP固定问题.


son*_*net 5

更好的方法 (imo) 是在(许多)不同values目录中定义不同的跨度计数,并让设备自动选择要使用的跨度计数。例如,

values/integers.xml -> span_count=3

values-w480dp/integers.xml -> span_count=4

values-w600dp/integers.xml -> span_count=5