How do I make WRAP_CONTENT work on a RecyclerView

后端 未结 19 1689
自闭症患者
自闭症患者 2020-11-22 12:49

I have a DialogFragment that contains a RecyclerView (a list of cards).

Within this RecyclerView are one or more CardVi

相关标签:
19条回答
  • 2020-11-22 13:44

    Replace measureScrapChild to follow code:

    private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
            int heightSpec, int[] measuredDimension)
        {
            View view = recycler.GetViewForPosition(position);
            if (view != null)
            {
                MeasureChildWithMargins(view, widthSpec, heightSpec);
                measuredDimension[0] = view.MeasuredWidth;
                measuredDimension[1] = view.MeasuredHeight;
                recycler.RecycleView(view);
            }
        }
    

    I use xamarin, so this is c# code. I think this can be easily "translated" to Java.

    0 讨论(0)
  • 2020-11-22 13:45

    i suggest you to put the recyclerview in any other layout (Relative layout is preferable). Then change recyclerview's height/width as match parent to that layout and set the parent layout's height/width as wrap content. it works for me

    0 讨论(0)
  • 2020-11-22 13:46

    This now works as they've made a release in version 23.2, as stated in this post. Quoting the official blogpost

    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.

    0 讨论(0)
  • 2020-11-22 13:47

    Here is the refined version of the class which seems to work and lacks problems other solutions have:

    package org.solovyev.android.views.llm;
    
    import android.content.Context;
    import android.support.v7.widget.RecyclerView;
    import android.util.Log;
    import android.view.View;
    
    /**
     * {@link android.support.v7.widget.LinearLayoutManager} which wraps its content. Note that this class will always
     * wrap the content regardless of {@link android.support.v7.widget.RecyclerView} layout parameters.
     *
     * Now it's impossible to run add/remove animations with child views which have arbitrary dimensions (height for
     * VERTICAL orientation and width for HORIZONTAL). However if child views have fixed dimensions
     * {@link #setChildSize(int)} method might be used to let the layout manager know how big they are going to be.
     * If animations are not used at all then a normal measuring procedure will run and child views will be measured during
     * the measure pass.
     */
    public class LinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {
    
        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 int childSize = DEFAULT_CHILD_SIZE;
        private boolean hasChildSize;
    
        @SuppressWarnings("UnusedDeclaration")
        public LinearLayoutManager(Context context) {
            super(context);
        }
    
        @SuppressWarnings("UnusedDeclaration")
        public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
            super(context, orientation, reverseLayout);
        }
    
        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 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, widthSpec, unspecified, childDimensions);
                        } else {
                            logMeasureWarning(i);
                        }
                    }
                    height += childDimensions[CHILD_HEIGHT];
                    if (i == 0) {
                        width = childDimensions[CHILD_WIDTH];
                    }
                    if (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, heightSpec, childDimensions);
                        } else {
                            logMeasureWarning(i);
                        }
                    }
                    width += childDimensions[CHILD_WIDTH];
                    if (i == 0) {
                        height = childDimensions[CHILD_HEIGHT];
                    }
                    if (width >= widthSize) {
                        break;
                    }
                }
            }
    
            if ((vertical && height < heightSize) || (!vertical && width < widthSize)) {
                // we really should wrap the contents of the view, let's do it
    
                if (exactWidth) {
                    width = widthSize;
                } else {
                    width += getPaddingLeft() + getPaddingRight();
                }
    
                if (exactHeight) {
                    height = heightSize;
                } else {
                    height += getPaddingTop() + getPaddingBottom();
                }
    
                setMeasuredDimension(width, height);
            } else {
                // if calculated height/width exceeds requested height/width let's use default "onMeasure" implementation
                super.onMeasure(recycler, state, widthSpec, heightSpec);
            }
        }
    
        private void logMeasureWarning(int child) {
            if (BuildConfig.DEBUG) {
                Log.w("LinearLayoutManager", "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 widthSpec, int heightSpec, int[] dimensions) {
            final View child = recycler.getViewForPosition(position);
    
            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;
    
            final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
            final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
    
            final int childWidthSpec = getChildMeasureSpec(widthSpec, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
            final int childHeightSpec = getChildMeasureSpec(heightSpec, 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;
    
            recycler.recycleView(child);
        }
    }
    

    This is also available as a library. Link to relevant class.

    0 讨论(0)
  • 2020-11-22 13:49

    Here is the c# version for mono android

    /* 
    * Ported by Jagadeesh Govindaraj (@jaganjan)
     *Copyright 2015 serso aka se.solovyev
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     * http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     *
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     *
     * Contact details
     *
     * Email: se.solovyev @gmail.com
     * Site:  http://se.solovyev.org
     */
    
    
    using Android.Content;
    using Android.Graphics;
    using Android.Support.V4.View;
    using Android.Support.V7.Widget;
    using Android.Util;
    using Android.Views;
    using Java.Lang;
    using Java.Lang.Reflect;
    using System;
    using Math = Java.Lang.Math;
    
    namespace Droid.Helper
    {
        public class WrapLayoutManager : LinearLayoutManager
        {
            private const int DefaultChildSize = 100;
            private static readonly Rect TmpRect = new Rect();
            private int _childSize = DefaultChildSize;
            private static bool _canMakeInsetsDirty = true;
            private static readonly int[] ChildDimensions = new int[2];
            private const int ChildHeight = 1;
            private const int ChildWidth = 0;
            private static bool _hasChildSize;
            private static  Field InsetsDirtyField = null;
            private static int _overScrollMode = ViewCompat.OverScrollAlways;
            private static RecyclerView _view;
    
            public WrapLayoutManager(Context context, int orientation, bool reverseLayout)
                : base(context, orientation, reverseLayout)
            {
                _view = null;
            }
    
            public WrapLayoutManager(Context context) : base(context)
            {
                _view = null;
            }
    
            public WrapLayoutManager(RecyclerView view) : base(view.Context)
            {
                _view = view;
                _overScrollMode = ViewCompat.GetOverScrollMode(view);
            }
    
            public WrapLayoutManager(RecyclerView view, int orientation, bool reverseLayout)
                : base(view.Context, orientation, reverseLayout)
            {
                _view = view;
                _overScrollMode = ViewCompat.GetOverScrollMode(view);
            }
    
            public void SetOverScrollMode(int overScrollMode)
            {
                if (overScrollMode < ViewCompat.OverScrollAlways || overScrollMode > ViewCompat.OverScrollNever)
                    throw new ArgumentException("Unknown overscroll mode: " + overScrollMode);
                if (_view == null) throw new ArgumentNullException(nameof(_view));
                _overScrollMode = overScrollMode;
                ViewCompat.SetOverScrollMode(_view, overScrollMode);
            }
    
            public static int MakeUnspecifiedSpec()
            {
                return View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified);
            }
    
            public override void OnMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec,
                int heightSpec)
            {
                var widthMode = View.MeasureSpec.GetMode(widthSpec);
                var heightMode = View.MeasureSpec.GetMode(heightSpec);
    
                var widthSize = View.MeasureSpec.GetSize(widthSpec);
                var heightSize = View.MeasureSpec.GetSize(heightSpec);
    
                var hasWidthSize = widthMode != MeasureSpecMode.Unspecified;
                var hasHeightSize = heightMode != MeasureSpecMode.Unspecified;
    
                var exactWidth = widthMode == MeasureSpecMode.Exactly;
                var exactHeight = heightMode == MeasureSpecMode.Exactly;
    
                var unspecified = MakeUnspecifiedSpec();
    
                if (exactWidth && exactHeight)
                {
                    // in case of exact calculations for both dimensions let's use default "onMeasure" implementation
                    base.OnMeasure(recycler, state, widthSpec, heightSpec);
                    return;
                }
    
                var vertical = Orientation == Vertical;
    
                InitChildDimensions(widthSize, heightSize, vertical);
    
                var width = 0;
                var 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();
    
                var stateItemCount = state.ItemCount;
                var adapterItemCount = ItemCount;
                // 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 (var 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[ChildHeight];
                        if (i == 0)
                        {
                            width = ChildDimensions[ChildWidth];
                        }
                        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[ChildWidth];
                        if (i == 0)
                        {
                            height = ChildDimensions[ChildHeight];
                        }
                        if (hasWidthSize && width >= widthSize)
                        {
                            break;
                        }
                    }
                }
    
                if (exactWidth)
                {
                    width = widthSize;
                }
                else
                {
                    width += PaddingLeft + PaddingRight;
                    if (hasWidthSize)
                    {
                        width = Math.Min(width, widthSize);
                    }
                }
    
                if (exactHeight)
                {
                    height = heightSize;
                }
                else
                {
                    height += PaddingTop + PaddingBottom;
                    if (hasHeightSize)
                    {
                        height = Math.Min(height, heightSize);
                    }
                }
    
                SetMeasuredDimension(width, height);
    
                if (_view == null || _overScrollMode != ViewCompat.OverScrollIfContentScrolls) return;
                var fit = (vertical && (!hasHeightSize || height < heightSize))
                          || (!vertical && (!hasWidthSize || width < widthSize));
    
                ViewCompat.SetOverScrollMode(_view, fit ? ViewCompat.OverScrollNever : ViewCompat.OverScrollAlways);
            }
    
            private void LogMeasureWarning(int child)
            {
    #if DEBUG
                Log.WriteLine(LogPriority.Warn, "LinearLayoutManager",
                    "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");
    #endif
            }
    
            private void InitChildDimensions(int width, int height, bool vertical)
            {
                if (ChildDimensions[ChildWidth] != 0 || ChildDimensions[ChildHeight] != 0)
                {
                    // already initialized, skipping
                    return;
                }
                if (vertical)
                {
                    ChildDimensions[ChildWidth] = width;
                    ChildDimensions[ChildHeight] = _childSize;
                }
                else
                {
                    ChildDimensions[ChildWidth] = _childSize;
                    ChildDimensions[ChildHeight] = height;
                }
            }
    
            public void ClearChildSize()
            {
                _hasChildSize = false;
                SetChildSize(DefaultChildSize);
            }
    
            public void SetChildSize(int size)
            {
                _hasChildSize = true;
                if (_childSize == size) return;
                _childSize = size;
                RequestLayout();
            }
    
            private void MeasureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize,
                int[] dimensions)
            {
                View child = null;
                try
                {
                    child = recycler.GetViewForPosition(position);
                }
                catch (IndexOutOfRangeException e)
                {
                    Log.WriteLine(LogPriority.Warn, "LinearLayoutManager",
                        "LinearLayoutManager doesn't work well with animations. Consider switching them off", e);
                }
    
                if (child != null)
                {
                    var p = child.LayoutParameters.JavaCast<RecyclerView.LayoutParams>()
    
                    var hPadding = PaddingLeft + PaddingRight;
                    var vPadding = PaddingTop + PaddingBottom;
    
                    var hMargin = p.LeftMargin + p.RightMargin;
                    var 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);
    
                    var hDecoration = GetRightDecorationWidth(child) + GetLeftDecorationWidth(child);
                    var vDecoration = GetTopDecorationHeight(child) + GetBottomDecorationHeight(child);
    
                    var childWidthSpec = GetChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.Width,
                        CanScrollHorizontally());
                    var childHeightSpec = GetChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.Height,
                        CanScrollVertically());
    
                    child.Measure(childWidthSpec, childHeightSpec);
    
                    dimensions[ChildWidth] = GetDecoratedMeasuredWidth(child) + p.LeftMargin + p.RightMargin;
                    dimensions[ChildHeight] = 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)
                    {
                       var klass = Java.Lang.Class.FromType (typeof (RecyclerView.LayoutParams));
                        InsetsDirtyField = klass.GetDeclaredField("mInsetsDirty");
                        InsetsDirtyField.Accessible = true;
                    }
                    InsetsDirtyField.Set(p, true);
                }
                catch (NoSuchFieldException e)
                {
                    OnMakeInsertDirtyFailed();
                }
                catch (IllegalAccessException e)
                {
                    OnMakeInsertDirtyFailed();
                }
            }
    
            private static void OnMakeInsertDirtyFailed()
            {
                _canMakeInsetsDirty = false;
    #if DEBUG
                Log.Warn("LinearLayoutManager",
                    "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
    #endif
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 13:49

    RecyclerView added support for wrap_content in 23.2.0 which was buggy , 23.2.1 was just stable , so you can use:

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

    You can see the revision history here:

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

    Note:

    Also note that after updating support library the RecyclerView will respect wrap_content as well as match_parent so if you have a Item View of a RecyclerView set as match_parent the single view will fill whole screen

    0 讨论(0)
提交回复
热议问题