美文网首页
ScrollView嵌套ListView显示不全分析及解决

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

作者: Leon_hy | 来源:发表于2019-12-25 23:57 被阅读0次

    1.发现问题

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


    image.pngimage.png Screenshot_2019-12-25-22-51-47-679_com.example.my.pngScreenshot_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.pngimage.png image.pngimage.png

    相关文章

      网友评论

          本文标题:ScrollView嵌套ListView显示不全分析及解决

          本文链接:https://www.haomeiwen.com/subject/jmnhoctx.html