ScrollView嵌套ListView显示不全分析及解决

1.发现问题

当我们使用ScrollView嵌套ListView时会出现ListView只显示一行的高度,如下图:
通过代码获取到ListView的高度是150,只有一行的高度:


image.png
Screenshot_2019-12-25-22-51-47-679_com.example.my.png

2.源码解析

为什么会出现这个问题呢?根据猜测应该是ListView在测量的时候出现了问题,我们找到ListView的onMeasure方法看下具体的源码。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取宽高的测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;
        //获取ListView的Item数量,遍历计算宽高 
        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState & MEASURED_STATE_MASK);
        }
        
        //由于是高度测量出现问题,我们只看高度的测量
        //我们看到当测量模式为 MeasureSpec.UNSPECIFIED时只计算了一行的高度
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }
        //我们看到当测量模式为 MeasureSpec.AT_MOST时,会通过measureHeightOfChildren累计
        //计算当前所有的item高度
        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);

        mWidthMeasureSpec = widthMeasureSpec;
    }

根据上源码我们知道,当测量模式为 MeasureSpec.UNSPECIFIED时只计算了一行的高度,而这个heightMode是它的parent(ScrollView)传进来的heightMeasureSpec计算的。我们继续看ScrollView,由于ScrollView继承自FrameLayout,我们来看下FrameLayout的onMeasure方法。

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //循环查找自己的子View如果子View不是GONE的话就调用ViewGroup的measureChildWithMargins方法
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        省略。。。。。。。
    }

代码比较简单就是循环查找自己的子View如果子View不是GONE的话就调用ViewGroup的measureChildWithMargins方法,我们发现ScrollView是重写了这个方法的,找到ScrollView的measureChildWithMargins方法。

 @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        //就是这里了 给子view传入的heightMeasureSpec是MeasureSpec.UNSPECIFIED
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

最终是ScrollView重写measureChildWithMargins后将MeasureSpec.UNSPECIFIED传到ListView的onMeasure方法测量。在最开始看到的ListView的onMeasure方法中

    //由于是高度测量出现问题,我们只看高度的测量,我们看到当测量模式为 MeasureSpec.UNSPECIFIED时只计算了一行的高度
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

当测量模式为 MeasureSpec.UNSPECIFIED就只会计算一行的高度,最后将高度通过setMeasuredDimension(widthSize, heightSize)方法设置ListView的高度,造成ListView显示不全。

3.解决办法

上述分析以及知道了显示不全的原因,我们只需将将ListView onMeasure方法的heightMeasureSpec模式为AT_MOST,让它进入下面的判断即可正常的显示高度了

//我们看到当测量模式为 MeasureSpec.AT_MOST时,会通过measureHeightOfChildren累计
        //计算当前显示多少条的item高度
        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

重写ListView的onMeasure方法

public class MyListView extends ListView {
    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,
                MeasureSpec.AT_MOST);

        super.onMeasure(widthMeasureSpec, heightSpec);
    }
}

运行代码获取高度,ListView显示完全:


image.png
image.png

你可能感兴趣的:(ScrollView嵌套ListView显示不全分析及解决)