Android RecyclerView GridLayoutManager:如何自动检测跨度计数?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/26666143/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
RecyclerView GridLayoutManager: how to auto-detect span count?
提问by foo64
Using the new GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html
使用新的 GridLayoutManager:https: //developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html
It takes an explicit span count, so the problem now becomes: how do you know how many "spans" fit per row? This is a grid, after all. There should be as many spans as the RecyclerView can fit, based on measured width.
它需要一个明确的跨度计数,所以现在的问题变成了:你怎么知道每行有多少个“跨度”?毕竟这是一个网格。根据测量的宽度,应该有 RecyclerView 可以容纳的尽可能多的跨度。
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:
使用旧的GridView
,您只需设置“columnWidth”属性,它就会自动检测适合的列数。这基本上是我想为 RecyclerView 复制的内容:
- add OnLayoutChangeListener on the
RecyclerView
- in this callback, inflate a single 'grid item' and measure it
- spanCount = recyclerViewWidth / singleItemWidth;
- 在上添加 OnLayoutChangeListener
RecyclerView
- 在此回调中,充气单个“网格项目”并对其进行测量
- spanCount = recyclerViewWidth / singleItemWidth;
This seems like pretty common behavior, so is there a simpler way that I'm not seeing?
这似乎是很常见的行为,所以有没有我没有看到的更简单的方法?
回答by s.maks
Personaly I don't like to subclass RecyclerView for this, because for me it seems that there is GridLayoutManager's responsibility to detect span count. So after some android source code digging for RecyclerView and GridLayoutManager I wrote my own class extended GridLayoutManager that do the job:
我个人不喜欢为此将 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);
}
}
I don't actually remember why I choosed to set span count in onLayoutChildren, I wrote this class some time ago. But the point is we need to do so after view get measured. so we can get it's height and width.
我其实不记得为什么我选择在 onLayoutChildren 中设置跨度计数,我前段时间写了这个类。但关键是我们需要在视图得到测量后这样做。所以我们可以得到它的高度和宽度。
EDIT 1:Fix error in code caused to incorrectly setting span count. Thanks user @Elyees Aboudafor reporting and suggesting solution.
编辑 1:修复因错误设置跨度计数而导致的代码错误。感谢用户@Elyees Abouda报告和建议解决方案。
EDIT 2:Some small refactoring and fix edge case with manual orientation changes handling. Thanks user @tatarizefor reporting and suggesting solution.
回答by speedynomads
I accomplished this using a view tree observer to get the width of the recylcerview once rendered and then getting the fixed dimensions of my card view from resources and then setting the span count after doing my calculations. It is only really applicable if the items you are displaying are of a fixed width. This helped me automatically populate the grid regardless of screen size or orientation.
我使用视图树观察器获取渲染后的 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();
}
});
回答by Ribs
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.
嗯,这就是我使用的,相当基本的,但为我完成了工作。这段代码基本上以倾角为单位获取屏幕宽度,然后除以 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);
回答by andrew_s
I extended the RecyclerView and overrode the onMeasure method.
我扩展了 RecyclerView 并覆盖了 onMeasure 方法。
I set an item width(member variable) as early as I can,with a default of 1. This also updates on configuration changed. This will now have as many rows as can fit in portrait,landscape,phone/tablet etc.
我尽可能早地设置了一个项目宽度(成员变量),默认值为 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);
}
}
}
回答by Ariq
I'm posting this just in case someone gets weird column width as in my case.
我发布这个只是为了防止有人像我一样得到奇怪的列宽。
I'm not able to comment on @s-marks's answer due to my low reputation. I applied his solution solutionbut I got some weird column width, so I modified checkedColumnWidthfunction as follows:
由于我的声誉很低,我无法对@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;
}
By converting the given column width into DP fixed the issue.
通过将给定的列宽转换为 DP 解决了这个问题。
回答by Andreas
To accommodate orientation change on s-marks's answer, I added a check on width change (width from getWidth(), not column width).
为了适应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);
}
回答by toncek
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:
upvoted 的解决方案很好,但将传入的值作为像素处理,如果您对测试和假设 dp 的值进行硬编码,这可能会让您失望。最简单的方法可能是将列宽放在一个维度中并在配置GridAutofitLayoutManager时读取它,它会自动将dp转换为正确的像素值:
new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
回答by Serjant.arbuz
- Set minimal fixed width of imageView (144dp x 144dp for example)
When you create GridLayoutManager, you need to know how much columns will be with minimal size of 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);
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); //Установка параметров ... }
- 设置 imageView 的最小固定宽度(例如 144dp x 144dp)
创建 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);
之后,如果列中有空间,则需要在适配器中调整 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); //Установка параметров ... }
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
它适用于两个方向。在垂直方向上我有 2 列,在水平方向上有 4 列。结果:https: //i.stack.imgur.com/WHvyD.jpg
回答by Tatarize
This is s.maks' class with a minor fix for when the recyclerview itself changes size. Such as when you deal with the orientation changes yourself (in the manifest android:configChanges="orientation|screenSize|keyboardHidden"
), or some other reason the recyclerview might change size without the mColumnWidth changing. I also changed the int value it takes to be the resource of the size and allowed a constructor of no resource then setColumnWidth to do that yourself.
这是 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;
}
}
}