如何自动检测跨度计数?

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

它需要显式的跨度计数,因此现在的问题是: 如何知道每行适合多少个“跨度”?毕竟这是个网格。根据测量的宽度,应该有回收视图能够容纳的尽可能多的跨度。

Using the old GridView, you would just set the "columnWidth" property and it would automatically detect how many columns fit. This is basically what I want to replicate for the RecyclerView:

  • RecyclerView上添加 OnLayoutChangeListener
  • in this callback, inflate a single 'grid item' and measure it
  • SpanCount = recyclerViewWidth/singleItemWidth;

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

110020 次浏览

将 SpanCount 设置为大数(即列的最大数目) ,并将自定义 SpanSizeLookup 设置为 GridLayoutManager。

mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int i) {
return SPAN_COUNT / (int) (mRecyclerView.getMeasuredWidth()/ CELL_SIZE_IN_PX);
}
});

It's a bit ugly, but it work.

我认为像 AutoSpanGridLayoutManager 这样的管理器是最好的解决方案,但我没有找到任何类似的东西。

编辑: 有一个错误,在一些设备上,它添加空白空间的权利

我使用了一个视图树观察者来获得一旦渲染后 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();
}
});

我扩展了侯赛因视图并覆盖了 on丈量方法。

我尽可能早地设置了一个条目宽度(成员变量) ,默认值为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);
}
}
}

Well, this is what I used, fairly basic, but gets the job done for me. This code basically gets the screen width in dips and then divides by 300 (or whatever width you're using for your adapter's layout). So smaller phones with 300-500 dip width only display one column, tablets 2-3 columns etc. Simple, fuss free and without downside, as far as I can see.

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

下面是我用来自动检测跨度计数的包装器的相关部分。通过使用 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();
}
}
}

就我个人而言,我不喜欢为此子类回收视图,因为对我来说,似乎有 GridLayoutManager 的责任来检测跨度计数。因此,在一些 android 源代码的回收视图和 GridLayoutManager 挖掘之后,我编写了自己的类扩展 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 中设置 span count,这是我前段时间写的类。但关键是,我们需要在观测结束后这样做。这样我们就能得到它的高度和宽度。

编辑1: 修正由于设置跨度计数错误而导致的代码错误。感谢用户 @ Elyees Abouda的报告和建议 solution

编辑2: 一些小的重构和修复手动方向改变处理的边缘情况。感谢用户 @ 鞑靼的报告和建议 解决方案

我发布这个只是为了防止有人像我一样得到奇怪的栏宽。

我不能评论 @ s-marks的答案,由于我的低声誉。我应用了他的解决方案 solution,但我得到了一些奇怪的列宽度,所以我修改了 校对柱宽函数如下:

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 解决了这个问题。

To accommodate orientation change on S-Marks's answer, I added a check on width change (width from getWidth(), not column width).

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

The upvoted solution is fine, but handles the incoming values as pixels, which can trip you up if you're hardcoding values for testing and assuming dp. Easiest way is probably to put the column width in a dimension and read it when configuring the GridAutofitLayoutManager, which will automatically convert dp to correct pixel value:

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. After that you need to resize imageView in adapter if you have space in column. You may send newImageViewSize then inisilize adapter from activity there you calculate screen and column count:

    @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); //Установка параметров
    ...
    }
    

It works in both orientations. In vertical I have 2 columns and in horizontal - 4 columns. The result: https://i.stack.imgur.com/WHvyD.jpg

我总结以上答案 here

这是 s.maks 的类,对于回收视图本身更改大小的情况有一个小的修复。例如,当您自己处理方向更改时(在清单 android:configChanges="orientation|screenSize|keyboardHidden"中) ,或者由于其他原因,回收视图可能会在不更改 mColumnWidth 的情况下更改大小。我还修改了 int 值作为 size 的资源,并允许没有资源的构造函数 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;
}
}
}

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

- > span_count=3

- > span_count=4

- > span_count=5

我喜欢 s.maks 的回答,但是我发现了另外一个极端的情况: 如果你将回收视图的高度设置为 WRAP _ CONTENT,可能会发生回收视图的高度根据一个过时的 span Count 值计算不正确的情况。我找到的解决方案是对 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);
}