121

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

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

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

  • 在上添加 OnLayoutChangeListenerRecyclerView
  • 在此回调中,为单个“网格项”充气并测​​量它
  • spanCount = recyclerViewWidth / singleItemWidth;

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

4

14 回答 14

139

就我个人而言,我不喜欢为此继承 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报告和建议解决方案

于 2015-05-15T10:12:59.120 回答
31

我使用视图树观察器来完成此操作,以获取渲染后的再循环视图的宽度,然后从资源中获取卡片视图的固定尺寸,然后在进行计算后设置跨度计数。只有当您显示的项目具有固定宽度时,它才真正适用。这帮助我自动填充网格,无论屏幕大小或方向如何。

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();
                }
            });
于 2014-11-18T17:53:39.593 回答
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);
于 2015-01-21T21:55:05.190 回答
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);
        }
    }
}
于 2014-12-10T22:37:33.927 回答
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 解决了这个问题。

于 2016-06-28T17:31:51.717 回答
7

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

values/integers.xml->span_count=3

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

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

于 2020-09-26T19:47:16.310 回答
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);
}
于 2017-02-15T05:58:53.540 回答
2

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

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
于 2017-05-05T12:39:57.317 回答
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

于 2017-05-28T10:27:58.430 回答
2

我在这里总结以上答案

于 2017-11-10T19:23:04.493 回答
2

这是 s.maks 的类,对 recyclerview 本身更改大小时进行了小修复。例如,当您自己处理方向更改时(在 manifest 中android:configChanges="orientation|screenSize|keyboardHidden"),或者由于其他原因,recyclerview 可能会更改大小而 mColumnWidth 没有更改。我还更改了作为大小资源所需的 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;
        }
    }
}
于 2018-12-15T12:37:17.200 回答
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);
}
于 2021-07-17T09:38:17.687 回答
-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 这样的经理将是最好的解决方案,但我没有找到类似的东西。

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

于 2014-11-12T12:58:51.400 回答
-6

这是我用来自动检测跨度计数的包装器的相关部分。您通过调用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();
        }
    }
}
于 2015-03-27T06:23:42.483 回答