Nested Recycler view height doesn't wrap its content

匿名 (未验证) 提交于 2019-12-03 01:27:01

问题:

I have an application that manage collections of books (like playlists).

I want to display a list of collection with a vertical RecyclerView and inside each row, a list of book in an horizontal RecyclerView.

When i set the layout_height of the inner horizontal RecyclerView to 300dp, it is displayed correctly but when i set it to wrap_content, it doesn't display anything. I need to use wrap_content because I want to be able to change the layout manager programmatically to switch between vertical and horizontal display.

Do you know what i'm doing wrong ?

My Fragment layout :

Collection element layout :

Book list item :

Here is my Collection Adapter :

private class CollectionsListAdapter extends RecyclerView.Adapter {     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();     } } 

And finally, the Book adapter :

private class BookListAdapter extends RecyclerView.Adapter 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();     } } 

回答1:

Update

Many issues relating to this feature in version 23.2.0 have been fixed in 23.2.1, update to that instead.

With the release of Support Library version 23.2, RecyclerView now supports that!

Update build.gradle to:

compile 'com.android.support:recyclerview-v7:23.2.1' 

or any version beyond that.

This release brings an exciting new feature to the LayoutManager API: auto-measurement! This allows a RecyclerView to size itself based on the size of its contents. This means that previously unavailable scenarios, such as using WRAP_CONTENT for a dimension of the RecyclerView, are now possible. You’ll find all built in LayoutManagers now support auto-measurement.

This can be disabled via setAutoMeasurementEnabled() if need be. Check in detail here.



回答2:

@user2302510 solution works not as good as you may expected. Full workaround for both orientations and dynamically data changes is:

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 


回答3:

The code up above doesn't work well when you need to make your items "wrap_content", because it measures both items height and width with MeasureSpec.UNSPECIFIED. After some troubles I've modified that solution so now items can expand. The only difference is that it provides parents height or width MeasureSpec depends on layout orientation.

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 


回答4:

Existing layout manager do not yet support wrap content.

You can create a new LayoutManager that extends the existing one and overrides onMeasure method to measure for wrap content.



回答5:

And other answers cannot scroll when there are more items than can be displayed on the screen though. This one is using default implemantation of onMeasure() when there are more items than screen size.

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 

And if you want to use it with GridLayoutManager just extends it from GridLayoutManager and change

for (int i = 0; i 

to

for (int i = 0; i 


回答6:

UPDATE March 2016

By Android Support Library 23.2.1 of a support library version. So all WRAP_CONTENT should work correctly.

Please update version of a library in gradle file.

compile 'com.android.support:recyclerview-v7:23.2.1' 

This allows a RecyclerView to size itself based on the size of its contents. This means that previously unavailable scenarios, such as using WRAP_CONTENT for a dimension of the RecyclerView, are now possible.

you’ll be required to call setAutoMeasureEnabled(true)

Fixed bugs related to various measure-spec methods in update

Check https://developer.android.com/topic/libraries/support-library/features.html



回答7:

This answer is based on the solution given by Denis Nek. It solves the problem of not taking decorations like dividers into account.

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 

}



回答8:

An alternative to extend LayoutManager can be just set the size of the view manually.

Number of items per row height (if all the items have the same height and the separator is included on the row)

LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mListView.getLayoutParams(); params.height = mAdapter.getItemCount() * getResources().getDimensionPixelSize(R.dimen.row_height); mListView.setLayoutParams(params); 

Is still a workaround, but for basic cases it works.



回答9:

Used solution from @sinan-kozak, except fixed a few bugs. Specifically, we shouldn't use View.MeasureSpec.UNSPECIFIED for both the width and height when calling measureScrapChild as that won't properly account for wrapped text in the child. Instead, we will pass through the width and height modes from the parent which will allow things to work for both horizontal and vertical layouts.

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 

`



回答10:

Here I have found a solution: https://code.google.com/p/android/issues/detail?id=74772

It is in no way my solution. I have just copied it from there, but I hope it will help someone as much as it helped me when implementing horizontal RecyclerView and wrap_content height (should work also for vertical one and wrap_content width)

The solution is to extend the LayoutManager and override its onMeasure method as @yigit suggested.

Here is the code in case the link dies:

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


回答11:

Android Support Library now handles WRAP_CONTENT property as well. Just import this in your gradle.

compile 'com.android.support:recyclerview-v7:23.2.0' 

And done!



回答12:

Based on the work of Denis Nek, it works well if the sum of item's widths is smaller than the size of the container. other than that, it will make the recyclerview non scrollable and only will show subset of the data.

to solve this problem, i modified the solution alittle so that it choose the min of the provided size and calculated size. see below:

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 


回答13:

I have tried all solutions, they are very useful but this only works fine for me

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 


回答14:

Yes the workaround shown in all answer is correct , that is we need to customize the linear layout manager to calculate the height of its child items dynamically at run time. But all answers not working as expected .Please the below answer for custom layout manger with all orientation support.

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_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 = heightSize) {                 break;             }         } else {             if (!hasChildSize) {                 if (i = 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 


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!