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

时间:2014-10-31 01:22:49

标签: android android-recyclerview android-gridlayout gridlayoutmanager

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

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

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

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

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

14 个答案:

答案 0 :(得分:105)

Personaly我不喜欢为此子类化RecyclerView,因为对我而言,GridLayoutManager似乎有责任检测跨度计数。因此,在为RecyclerView和GridLayoutManager挖掘一些android源代码后,我编写了自己的类扩展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);
    }
}

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

编辑:修复因错误设置跨度计数导致的代码错误。感谢用户@Elyees Abouda报告并建议solution

答案 1 :(得分:25)

我使用视图树观察器完成了这一操作,一旦渲染后获取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 :(得分: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);
        }
    }
}

答案 3 :(得分:13)

嗯,这是我用过的,相当基本的,但是为我完成了工作。此代码基本上以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);

答案 4 :(得分:7)

我发布这个以防万一有人像我的情况那样得到奇怪的列宽。

由于声誉不佳,我无法对@s-marks的答案发表评论。我应用了他的解决方案solution,但我得到了一些奇怪的列宽,所以我修改了 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,可以解决问题。

答案 5 :(得分:2)

upvoted解决方案很好,但是将输入值作为像素处理,如果您正在硬编码测试值并假设dp,这可能会让您失望。最简单的方法可能是将列宽放在一个维度中,并在配置GridAutofitLayoutManager时读取它,这将自动将dp转换为正确的像素值:

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

答案 6 :(得分:2)

我的结论是here

答案 7 :(得分:2)

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

values/integers.xml-> span_count=3

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

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

答案 8 :(得分:1)

为了适应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);
}

答案 9 :(得分:1)

  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); //Установка параметров
       ...
    }
    
  4. 它适用于两个方向。在垂直方向,我有2列,水平 - 4列。结果:https://i.stack.imgur.com/WHvyD.jpg

答案 10 :(得分:0)

这是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;
        }
    }
}

答案 11 :(得分:0)

我喜欢 s.maks 的回答,但我发现了另一个极端情况:如果将 RecyclerView 的高度设置为 WRAP_CONTENT,则可能会根据过时的 spanCount 值错误地计算 recyclerview 的高度。我发现的解决方案是对提议的 onLayoutChildren() 方法的一个小修改:

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 || getOrientation() == HORIZONTAL) && (height > 0 || getOrientation() == VERTICAL) && (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);
        if (getSpanCount() != spanCount) {
            setSpanCount(spanCount);
        }
        isColumnWidthChanged = false;
    }
    lastWidth = width;
    lastHeight = height;
    super.onLayoutChildren(recycler, state);
}

答案 12 :(得分:-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这样的经理会是最好的解决方案,但我没有找到类似的东西。

编辑:有一个错误,在某些设备上它会向右侧添加空格

答案 13 :(得分:-7)

这里是我用来自动检测跨度计数的包装器的相关部分。您可以通过使用 R.layout.my_grid_item 引用调用setGridLayoutManager来初始化它,并确定每行中可以容纳多少个。

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