嵌套回收器视图高度不包含其内容
Posted
技术标签:
【中文标题】嵌套回收器视图高度不包含其内容【英文标题】:Nested Recycler view height doesn't wrap its content 【发布时间】:2014-12-26 06:46:18 【问题描述】:我有一个应用程序来管理图书收藏(如播放列表)。
我想显示一个带有垂直 RecyclerView 的集合列表,并在每行内显示一个水平 RecyclerView 中的书籍列表。
当我将内部水平 RecyclerView 的 layout_height 设置为 300dp 时,它可以正确显示,但是 当我将它设置为 wrap_content 时,它什么也不显示。 我需要使用 wrap_content 因为我希望能够以编程方式更改布局管理器以在垂直和水平显示之间切换。
你知道我做错了什么吗?
我的片段布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:background="@color/white">
<com.twibit.ui.view.CustomSwipeToRefreshLayout
android:id="@+id/swipe_container"
android:layout_
android:layout_>
<LinearLayout
android:layout_
android:layout_
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/shelf_collection_listview"
android:layout_
android:layout_
android:paddingTop="10dp"/>
</LinearLayout>
</com.twibit.ui.view.CustomSwipeToRefreshLayout>
</LinearLayout>
集合元素布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:orientation="vertical">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:background="#FFF">
<!-- Simple Header -->
</RelativeLayout>
<FrameLayout
android:layout_
android:layout_>
<TextView
android:layout_
android:layout_
android:text="@string/empty_collection"
android:id="@+id/empty_collection_tv"
android:visibility="gone"
android:gravity="center"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/collection_book_listview"
android:layout_
android:layout_/> <!-- android:layout_ -->
</FrameLayout>
</LinearLayout>
书单项:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:layout_gravity="center">
<ImageView
android:id="@+id/shelf_item_cover"
android:layout_
android:layout_
android:layout_gravity="center"
android:maxWidth="150dp"
android:maxHeight="200dp"
android:src="@drawable/placeholder"
android:contentDescription="@string/cover"
android:adjustViewBounds="true"
android:background="@android:drawable/dialog_holo_light_frame"/>
</FrameLayout>
这是我的收集适配器:
private class CollectionsListAdapter extends RecyclerView.Adapter<CollectionsListAdapter.ViewHolder>
private final String TAG = CollectionsListAdapter.class.getSimpleName();
private Context mContext;
// Create the ViewHolder class to keep references to your views
class ViewHolder extends RecyclerView.ViewHolder
private final TextView mHeaderTitleTextView;
private final TextView mHeaderCountTextView;
private final RecyclerView mHorizontalListView;
private final TextView mEmptyTextView;
public ViewHolder(View view)
super(view);
mHeaderTitleTextView = (TextView) view.findViewById(R.id.collection_header_tv);
mHeaderCountTextView = (TextView) view.findViewById(R.id.collection_header_count_tv);
mHorizontalListView = (RecyclerView) view.findViewById(R.id.collection_book_listview);
mEmptyTextView = (TextView) view.findViewById(R.id.empty_collection_tv);
public CollectionsListAdapter(Context context)
mContext = context;
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int i)
Log.d(TAG, "CollectionsListAdapter.onCreateViewHolder(" + parent.getId() + ", " + i + ")");
// Create a new view by inflating the row item xml.
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.shelf_collection, parent, false);
// Set the view to the ViewHolder
ViewHolder holder = new ViewHolder(v);
holder.mHorizontalListView.setHasFixedSize(false);
holder.mHorizontalListView.setHorizontalScrollBarEnabled(true);
// use a linear layout manager
LinearLayoutManager mLayoutManager = new LinearLayoutManager(mContext);
mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
holder.mHorizontalListView.setLayoutManager(mLayoutManager);
return holder;
@Override
public void onBindViewHolder(ViewHolder holder, int i)
Log.d(TAG, "CollectionsListAdapter.onBindViewHolder(" + holder.getPosition() + ", " + i + ")");
Collection collection = mCollectionList.get(i);
Log.d(TAG, "Collection : " + collection.getLabel());
holder.mHeaderTitleTextView.setText(collection.getLabel());
holder.mHeaderCountTextView.setText("" + collection.getBooks().size());
// Create an adapter if none exists
if (!mBookListAdapterMap.containsKey(collection.getCollectionId()))
mBookListAdapterMap.put(collection.getCollectionId(), new BookListAdapter(getActivity(), collection));
holder.mHorizontalListView.setAdapter(mBookListAdapterMap.get(collection.getCollectionId()));
@Override
public int getItemCount()
return mCollectionList.size();
最后是 Book 适配器:
private class BookListAdapter extends RecyclerView.Adapter<BookListAdapter.ViewHolder> implements View.OnClickListener
private final String TAG = BookListAdapter.class.getSimpleName();
// Create the ViewHolder class to keep references to your views
class ViewHolder extends RecyclerView.ViewHolder
public ImageView mCoverImageView;
public ViewHolder(View view)
super(view);
mCoverImageView = (ImageView) view.findViewById(R.id.shelf_item_cover);
@Override
public void onClick(View v)
BookListAdapter.ViewHolder holder = (BookListAdapter.ViewHolder) v.getTag();
int position = holder.getPosition();
final Book book = mCollection.getBooks().get(position);
// Click on cover image
if (v.getId() == holder.mCoverImageView.getId())
downloadOrOpenBook(book);
return;
private void downloadOrOpenBook(final Book book)
// do stuff
private Context mContext;
private Collection mCollection;
public BookListAdapter(Context context, Collection collection)
Log.d(TAG, "BookListAdapter(" + context + ", " + collection + ")");
mCollection = collection;
mContext = context;
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int i)
Log.d(TAG, "onCreateViewHolder(" + parent.getId() + ", " + i + ")");
// Create a new view by inflating the row item xml.
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.shelf_grid_item, parent, false);
// Set the view to the ViewHolder
ViewHolder holder = new ViewHolder(v);
holder.mCoverImageView.setOnClickListener(BookListAdapter.this); // Download or Open
holder.mCoverImageView.setTag(holder);
return holder;
@Override
public void onBindViewHolder(ViewHolder holder, int i)
Log.d(TAG, "onBindViewHolder(" + holder.getPosition() + ", " + i + ")");
Book book = mCollection.getBooks().get(i);
ImageView imageView = holder.mCoverImageView;
ImageLoader.getInstance().displayImage(book.getCoverUrl(), imageView);
@Override
public int getItemCount()
return mCollection.getBooks().size();
【问题讨论】:
为什么要使用wrap_content来改变layoutmanager的纵横? 如果我将 layout_height 设置为一个特定的值(比如说水平布局管理器的元素高度),它只显示垂直布局管理器列表的第一行。 但是 recyclerView 有一个内置的滚动视图,所以你可以滚动浏览项目......或者我错过了这里的重点?抱歉试图理解 我想滚动浏览收藏项目,但不想滚动内部图书列表(水平方向除外)。像 ExpandableListView 之类的东西。 我和你有同样的问题,你现在解决了吗? 【参考方案1】:@user2302510 解决方案的效果不如您预期的那么好。方向和动态数据更改的完整解决方法是:
public class MyLinearLayoutManager extends LinearLayoutManager
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
if (getOrientation() == HORIZONTAL)
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
【讨论】:
您的解决方案运行良好,但似乎没有考虑 ItemDecoration(例如分隔符)。 不知道为什么,但滚动似乎不起作用。我用垂直滚动布局尝试了这段代码......当项目小于屏幕高度时,一切正常。但是,当屏幕上无法显示的项目多时,滚动不起作用。 这不适用于内容更改动画 - recyclerview 的大小会捕捉而不是平滑地增长/缩小。 这是该类的一个改进版本,它似乎可以工作并且没有其他解决方案存在的问题:github.com/serso/android-linear-layout-manager/blob/master/lib/… 我在滚动视图中使用这个 recyclerview 并使用这个代码使 recyclerview 包装内容。但是滚动scrollview并不顺畅。解决方案是***.com/a/32283439/3736955【参考方案2】:更新
23.2.0 版本中与此功能相关的许多问题已在 23.2.1 中修复,请改为更新。
随着支持库版本 23.2 的发布,RecyclerView
现在支持它!
将build.gradle
更新为:
compile 'com.android.support:recyclerview-v7:23.2.1'
或除此之外的任何版本。
此版本为 LayoutManager API 带来了一项激动人心的新功能:自动测量!这允许 RecyclerView 根据其内容的大小来调整自己的大小。这意味着以前不可用的场景,例如将 WRAP_CONTENT 用于 RecyclerView 的维度,现在是可能的。您会发现所有内置的 LayoutManagers 现在都支持自动测量。
如果需要,可以通过setAutoMeasurementEnabled()
禁用此功能。详细查看here。
【讨论】:
小心!以下是新错误:***.com/questions/35619022/… 这是否解决了上面的错误? 这不能正常工作总是看到每个项目之间的空白 请注意包含子回收器视图的父布局的高度必须是包装内容。 版本23.2.1
的 RecyclerView 似乎已经修复了几个错误。对我们来说效果很好。如果没有,您甚至可以尝试使用24.0.0-alpha1
【参考方案3】:
当您需要将项目设置为“wrap_content”时,上面的代码无法正常工作,因为它使用 MeasureSpec.UNSPECIFIED 测量项目的高度和宽度。经过一些麻烦后,我修改了该解决方案,因此现在项目可以扩展。唯一的区别是它为父级提供的高度或宽度 MeasureSpec 取决于布局方向。
public class MyLinearLayoutManager extends LinearLayoutManager
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
if (getOrientation() == HORIZONTAL)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
heightSpec,
mMeasuredDimension);
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
measureScrapChild(recycler, i,
widthSpec,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
recycler.bindViewToPosition(view, position);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
【讨论】:
谢谢!这个解决方案对我来说效果最好。丹尼斯的测量似乎不够,视图仍然可以滚动。司南只有在有一个孩子的时候才会表现得很好。注意:我的子视图使用了wrap_content
要在到达视图父级边界时启用滚动,我必须输入switch (heightMode) case View.MeasureSpec.AT_MOST: if (height < heightSize) break; case View.MeasureSpec.EXACTLY: height = heightSize; case View.MeasureSpec.UNSPECIFIED:
优秀的解决方案。这非常有效。附带说明一下,我没有使用构造函数,而是创建了一个私有实例变量和一个带有 Constant final int ORIENTATION 的 setter/getter 方法(以防万一其他人遇到问题),然后用它来检查传递的方向.
我在 Lollipop 及更高版本上遇到了问题,当您尝试在 RecyclerView 上方滚动时,包裹 RecyclerView 的 ScrollView 工作不正确(没有弹跳)。要解决此问题,您需要制作 NotScrollableRecyclerView(自定义)并覆盖 onInterceptTouchEvent、onTouchEvent 方法而不调用它们的超级实现,并且您应该在两种方法中都返回 false。
这行得通,但我不明白为什么在计算出孩子的身高后让这个东西测量它的高度是如此不必要的复杂......【参考方案4】:
现有的布局管理器还不支持换行内容。
您可以创建一个新的 LayoutManager 来扩展现有的并覆盖 onMeasure 方法来测量包装内容。
【讨论】:
这不是我所期望的,但它似乎是真的。见:RecyclerView.LayoutManager & LinearLayoutManager @yiğit , recyclerView 会默认支持包装内容吗? 它依赖于 LayoutManager 而不是 RecyclerView。它在我们的路线图中。 @yigit 这难道不会使setHasFixedSize(boolean)
对默认布局管理器无用吗?无论如何,很高兴知道它在路线图上。希望快点出来。
是的,因为如果 LM 的大小取决于适配器内容,则它不能具有固定大小。主要区别在于 RV 是在 onMeasure 之前还是在 onLayout 之前处理更新。此外,通过调整大小,很难进行预测动画,因为 LM 需要知道最终大小才能正确测量自身,而预布局将在测量之后发生(就像回滚时间一样)。【参考方案5】:
正如@yiğit 提到的,您需要覆盖 onMeasure()。 @user2302510 和 @DenisNek 都有很好的答案,但如果你想支持 ItemDecoration,你可以使用这个自定义布局管理器。
但是,当屏幕上显示的项目过多时,其他答案将无法滚动。当项目多于屏幕大小时,此选项使用 onMeasure() 的默认实现。
public class MyLinearLayoutManager extends LinearLayoutManager
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
if (getOrientation() == HORIZONTAL)
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
// If child view is more than screen size, there is no need to make it wrap content. We can use original onMeasure() so we can scroll view.
if (height < heightSize && width < widthSize)
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
else
super.onMeasure(recycler, state, widthSpec, heightSpec);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
// For adding Item Decor Insets to view
super.measureChildWithMargins(view, 0, 0);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight() + getDecoratedLeft(view) + getDecoratedRight(view), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom() + getPaddingBottom() + getDecoratedBottom(view) , p.height);
view.measure(childWidthSpec, childHeightSpec);
// Get decorated measurements
measuredDimension[0] = getDecoratedMeasuredWidth(view) + p.leftMargin + p.rightMargin;
measuredDimension[1] = getDecoratedMeasuredHeight(view) + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
如果你想将它与 GridLayoutManager 一起使用,只需从 GridLayoutManager 扩展它并更改
for (int i = 0; i < getItemCount(); i++)
到
for (int i = 0; i < getItemCount(); i = i + getSpanCount())
【讨论】:
这是正常工作的解决方案,我已经测试了这个线程中的所有其他人,他们都有问题 这个解决方案对我也很有效。唯一的问题是它没有正确解释孩子中的包装文本。有关该问题的解决方案,请参见我上面的答案。 这对 gridlayout 不起作用(是的,我应用了更改)。首先它似乎工作,然后我来回改变方向,并且与原始高度相比高度发生了变化。此外,视图的行为也与预期不符(例如,大多数情况下 onclicks 都不起作用,而使用简单的 gridlayoutmanager 他们可以工作)。 考虑到跨度计数,我们如何才能为gridlayoutmanager
和 staggeredgridlayoutmanager
实现相同的目标?
这个 MyLinearLayoutManager 应该应用于子回收器视图还是父视图?还是两者兼而有之?【参考方案6】:
2016 年 3 月更新
由支持库版本的Android Support Library 23.2.1 提供。所以所有 WRAP_CONTENT 都应该可以正常工作。
请更新 gradle 文件中的库版本。
compile 'com.android.support:recyclerview-v7:23.2.1'
这允许 RecyclerView 根据其内容的大小来调整自己的大小。这意味着以前不可用的场景,例如将 WRAP_CONTENT 用于 RecyclerView 的维度,现在可以实现。
您需要调用 setAutoMeasureEnabled(true)
修复了与更新中的各种测量规范方法相关的错误
查看https://developer.android.com/topic/libraries/support-library/features.html
【讨论】:
【参考方案7】:此答案基于 Denis Nek 给出的解决方案。解决了不考虑隔板等装饰的问题。
public class WrappingRecyclerViewLayoutManager extends LinearLayoutManager
public WrappingRecyclerViewLayoutManager(Context context)
super(context, VERTICAL, false);
public WrappingRecyclerViewLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
if (getOrientation() == HORIZONTAL)
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
Rect outRect = new Rect();
calculateItemDecorationsForChild(view, outRect);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin + outRect.bottom + outRect.top;
recycler.recycleView(view);
【讨论】:
最后,唯一能正常工作的。做得好。谢谢! 我收到 java.lang.IndexOutOfBoundsException:无效的项目位置 0(0)。项目数:0 个例外。【参考方案8】:扩展 LayoutManager 的替代方法是手动设置视图的大小。
每行高度的项目数(如果所有项目的高度相同并且行中包含分隔符)
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mListView.getLayoutParams();
params.height = mAdapter.getItemCount() * getResources().getDimensionPixelSize(R.dimen.row_height);
mListView.setLayoutParams(params);
仍然是一种解决方法,但对于基本情况它有效。
【讨论】:
【参考方案9】:这里我找到了解决办法:https://code.google.com/p/android/issues/detail?id=74772
这绝不是我的解决方案。我刚刚从那里复制了它,但我希望它在实现水平 RecyclerView 和 wrap_content 高度(也应该适用于垂直和 wrap_content 宽度)时对我有所帮助
解决方案是扩展 LayoutManager 并按照@yigit 的建议覆盖其 onMeasure 方法。
这是链接失效时的代码:
public static class MyLinearLayoutManager extends LinearLayoutManager
public MyLinearLayoutManager(Context context)
super(context);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
measureScrapChild(recycler, 0,
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
int width = mMeasuredDimension[0];
int height = mMeasuredDimension[1];
switch (widthMode)
case View.MeasureSpec.EXACTLY:
case View.MeasureSpec.AT_MOST:
width = widthSize;
break;
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
case View.MeasureSpec.AT_MOST:
height = heightSize;
break;
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth();
measuredDimension[1] = view.getMeasuredHeight();
recycler.recycleView(view);
【讨论】:
宾果游戏!水平完美,但垂直不完美【参考方案10】:使用来自@sinan-kozak 的解决方案,除了修复了一些错误。具体来说,我们不应该在调用measureScrapChild
时将View.MeasureSpec.UNSPECIFIED
用于both 宽度和高度,因为这不能正确解释子级中的包装文本。相反,我们将从父级传递宽度和高度模式,这将允许水平和垂直布局都适用。
public class MyLinearLayoutManager extends LinearLayoutManager
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
if (getOrientation() == HORIZONTAL)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(heightSize, heightMode),
mMeasuredDimension);
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(widthSize, widthMode),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
// If child view is more than screen size, there is no need to make it wrap content. We can use original onMeasure() so we can scroll view.
if (height < heightSize && width < widthSize)
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
else
super.onMeasure(recycler, state, widthSpec, heightSpec);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
// For adding Item Decor Insets to view
super.measureChildWithMargins(view, 0, 0);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight() + getDecoratedLeft(view) + getDecoratedRight(view), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom() + getDecoratedTop(view) + getDecoratedBottom(view) , p.height);
view.measure(childWidthSpec, childHeightSpec);
// Get decorated measurements
measuredDimension[0] = getDecoratedMeasuredWidth(view) + p.leftMargin + p.rightMargin;
measuredDimension[1] = getDecoratedMeasuredHeight(view) + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
`
【讨论】:
我已经测试过了,它可以工作,但前提是RecyclerView
嵌套在 LinearLayout
中。不适用于RelativeLayout
。【参考方案11】:
是的,所有答案中显示的解决方法都是正确的,即我们需要自定义线性布局管理器以在运行时动态计算其子项的高度。但是所有答案都没有按预期工作。请在下面回答具有所有方向支持的自定义布局管理器。
public class MyLinearLayoutManager extends android.support.v7.widget.LinearLayoutManager
private static boolean canMakeInsetsDirty = true;
private static Field insetsDirtyField = null;
private static final int CHILD_WIDTH = 0;
private static final int CHILD_HEIGHT = 1;
private static final int DEFAULT_CHILD_SIZE = 100;
private final int[] childDimensions = new int[2];
private final RecyclerView view;
private int childSize = DEFAULT_CHILD_SIZE;
private boolean hasChildSize;
private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS;
private final Rect tmpRect = new Rect();
@SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context)
super(context);
this.view = null;
@SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
this.view = null;
@SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view)
super(view.getContext());
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
@SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout)
super(view.getContext(), orientation, reverseLayout);
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
public void setOverScrollMode(int overScrollMode)
if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER)
throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode);
if (this.view == null) throw new IllegalStateException("view == null");
this.overScrollMode = overScrollMode;
ViewCompat.setOverScrollMode(view, overScrollMode);
public static int makeUnspecifiedSpec()
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;
final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;
final int unspecified = makeUnspecifiedSpec();
if (exactWidth && exactHeight)
// in case of exact calculations for both dimensions let's use default "onMeasure" implementation
super.onMeasure(recycler, state, widthSpec, heightSpec);
return;
final boolean vertical = getOrientation() == VERTICAL;
initChildDimensions(widthSize, heightSize, vertical);
int width = 0;
int height = 0;
// it's possible to get scrap views in recycler which are bound to old (invalid) adapter entities. This
// happens because their invalidation happens after "onMeasure" method. As a workaround let's clear the
// recycler now (it should not cause any performance issues while scrolling as "onMeasure" is never
// called whiles scrolling)
recycler.clear();
final int stateItemCount = state.getItemCount();
final int adapterItemCount = getItemCount();
// adapter always contains actual data while state might contain old data (f.e. data before the animation is
// done). As we want to measure the view with actual data we must use data from the adapter and not from the
// state
for (int i = 0; i < adapterItemCount; i++)
if (vertical)
if (!hasChildSize)
if (i < stateItemCount)
// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
// we will use previously calculated dimensions
measureChild(recycler, i, widthSize, unspecified, childDimensions);
else
logMeasureWarning(i);
height += childDimensions[CHILD_HEIGHT];
if (i == 0)
width = childDimensions[CHILD_WIDTH];
if (hasHeightSize && height >= heightSize)
break;
else
if (!hasChildSize)
if (i < stateItemCount)
// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
// we will use previously calculated dimensions
measureChild(recycler, i, unspecified, heightSize, childDimensions);
else
logMeasureWarning(i);
width += childDimensions[CHILD_WIDTH];
if (i == 0)
height = childDimensions[CHILD_HEIGHT];
if (hasWidthSize && width >= widthSize)
break;
if (exactWidth)
width = widthSize;
else
width += getPaddingLeft() + getPaddingRight();
if (hasWidthSize)
width = Math.min(width, widthSize);
if (exactHeight)
height = heightSize;
else
height += getPaddingTop() + getPaddingBottom();
if (hasHeightSize)
height = Math.min(height, heightSize);
setMeasuredDimension(width, height);
if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS)
final boolean fit = (vertical && (!hasHeightSize || height < heightSize))
|| (!vertical && (!hasWidthSize || width < widthSize));
ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS);
private void logMeasureWarning(int child)
if (BuildConfig.DEBUG)
Log.w("MyLinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
"To remove this message either use #setChildSize() method or don't run RecyclerView animations");
private void initChildDimensions(int width, int height, boolean vertical)
if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0)
// already initialized, skipping
return;
if (vertical)
childDimensions[CHILD_WIDTH] = width;
childDimensions[CHILD_HEIGHT] = childSize;
else
childDimensions[CHILD_WIDTH] = childSize;
childDimensions[CHILD_HEIGHT] = height;
@Override
public void setOrientation(int orientation)
// might be called before the constructor of this class is called
//noinspection ConstantConditions
if (childDimensions != null)
if (getOrientation() != orientation)
childDimensions[CHILD_WIDTH] = 0;
childDimensions[CHILD_HEIGHT] = 0;
super.setOrientation(orientation);
public void clearChildSize()
hasChildSize = false;
setChildSize(DEFAULT_CHILD_SIZE);
public void setChildSize(int childSize)
hasChildSize = true;
if (this.childSize != childSize)
this.childSize = childSize;
requestLayout();
private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions)
final View child;
try
child = recycler.getViewForPosition(position);
catch (IndexOutOfBoundsException e)
if (BuildConfig.DEBUG)
Log.w("MyLinearLayoutManager", "MyLinearLayoutManager doesn't work well with animations. Consider switching them off", e);
return;
final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();
final int hPadding = getPaddingLeft() + getPaddingRight();
final int vPadding = getPaddingTop() + getPaddingBottom();
final int hMargin = p.leftMargin + p.rightMargin;
final int vMargin = p.topMargin + p.bottomMargin;
// we must make insets dirty in order calculateItemDecorationsForChild to work
makeInsetsDirty(p);
// this method should be called before any getXxxDecorationXxx() methods
calculateItemDecorationsForChild(child, tmpRect);
final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically());
child.measure(childWidthSpec, childHeightSpec);
dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;
// as view is recycled let's not keep old measured values
makeInsetsDirty(p);
recycler.recycleView(child);
private static void makeInsetsDirty(RecyclerView.LayoutParams p)
if (!canMakeInsetsDirty)
return;
try
if (insetsDirtyField == null)
insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty");
insetsDirtyField.setAccessible(true);
insetsDirtyField.set(p, true);
catch (NoSuchFieldException e)
onMakeInsertDirtyFailed();
catch (IllegalAccessException e)
onMakeInsertDirtyFailed();
private static void onMakeInsertDirtyFailed()
canMakeInsetsDirty = false;
if (BuildConfig.DEBUG)
Log.w("MyLinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
【讨论】:
【参考方案12】:Android 支持库现在也可以处理 WRAP_CONTENT 属性。只需将其导入您的 gradle 即可。
compile 'com.android.support:recyclerview-v7:23.2.0'
完成了!
【讨论】:
【参考方案13】:根据 Denis Nek 的作品, 如果项目宽度的总和小于容器的大小,则效果很好。除此之外,它会使 recyclerview 不可滚动,并且只会显示数据的子集。
为了解决这个问题,我稍微修改了解决方案,以便它选择提供的尺寸和计算尺寸的最小值。见下文:
package com.linkdev.gafi.adapters;
import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import com.linkdev.gafi.R;
public class MyLinearLayoutManager extends LinearLayoutManager
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
this.c = context;
private Context c;
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
if (getOrientation() == HORIZONTAL)
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
int widthDesired = Math.min(widthSize,width);
setMeasuredDimension(widthDesired, height);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
【讨论】:
【参考方案14】:我已经尝试了所有解决方案,它们非常有用 但这只对我有用
public class LinearLayoutManager extends android.support.v7.widget.LinearLayoutManager
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout)
super(context, orientation, reverseLayout);
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec)
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++)
if (getOrientation() == HORIZONTAL)
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
heightSpec,
mMeasuredDimension);
width = width + mMeasuredDimension[0];
if (i == 0)
height = mMeasuredDimension[1];
else
measureScrapChild(recycler, i,
widthSpec,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
height = height + mMeasuredDimension[1];
if (i == 0)
width = mMeasuredDimension[0];
if (height < heightSize || width < widthSize)
switch (widthMode)
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
switch (heightMode)
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
setMeasuredDimension(width, height);
else
super.onMeasure(recycler, state, widthSpec, heightSpec);
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension)
View view = recycler.getViewForPosition(position);
recycler.bindViewToPosition(view, position);
if (view != null)
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
【讨论】:
【参考方案15】:使用带有网格布局的 RecyclerView 简单地包装内容
图片: Recycler as GridView layout
只需像这样使用 GridLayoutManager:
RecyclerView.LayoutManager mRecyclerGrid=new GridLayoutManager(this,3,LinearLayoutManager.VERTICAL,false);
mRecyclerView.setLayoutManager(mRecyclerGrid);
您可以设置一行应显示多少个项目(替换 3 个)。
【讨论】:
以上是关于嵌套回收器视图高度不包含其内容的主要内容,如果未能解决你的问题,请参考以下文章
根据内容动态设置 UIScrollView 高度和 UITableView 高度
如何在带有 Flutter 的可滚动视图中拥有具有可变高度内容的 TabView?