美文网首页Android开发经验谈Android开发Android知识
从源码角度分析SrcollView嵌套ListView显示不全的

从源码角度分析SrcollView嵌套ListView显示不全的

作者: _Cancer | 来源:发表于2017-09-02 16:15 被阅读194次

    从源码角度分析SrcollView嵌套ListView显示不全的问题

    Sunxin's Bolg
    Sunxin's Github

    问题描述

    在之前开发的时候会碰到列表滑动布局中ScrollView嵌套ListView的情况,当嵌套了之后发现ListView只能显示一行数据。碰到这种情况也是

    真让人头大

    于是乎开始Google,很快就找到解决方法,自定义view继承自ListView,重写onMeasure()方法,然后再加入一行代码即可解决问题。

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

    But... why?

    真让人摸不着头脑

    问题分析

    首先应该可以想到,ScrollView嵌套ListView,ListView相当于ScrollView的子View了,我们应该去看看父View如何去测量子View的,打开源码先看ScrollView的onMeasure方法。

        
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
        if (!mFillViewport) {
            return;
        }
    
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }
    
        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            final int widthPadding;
            final int heightPadding;
            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (targetSdkVersion >= VERSION_CODES.M) {
                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
            } else {
                widthPadding = mPaddingLeft + mPaddingRight;
                heightPadding = mPaddingTop + mPaddingBottom;
            }
    
            final int desiredHeight = getMeasuredHeight() - heightPadding;
            if (child.getMeasuredHeight() < desiredHeight) {
                final int childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, widthPadding, lp.width);
                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredHeight, MeasureSpec.EXACTLY);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
    

    发现第一行就调用了父类的onMeasure方法,点进去看看做了什么,ScrollView的父类是FrameLayout

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

    截取了一部分代码,看呀看,发现了一个很可疑的方法

    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
    
    

    看名称是测量Child的,跟进,发现进入了ViewGroup里面。

    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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    

    一眼望去,感觉应该在里面计算子View的宽高,并且把padding值和margin值都算进去了。不管那么多了,ViewGroup是ScrollView的爷爷了吧,ScrollView里面应该会重写这个方法,因为ScrollView里面也能包裹其他的View,跟进。

    @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;
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    

    果然,这样一来ListView被ScrollView包裹,它的宽高应该是在这里被计算的,看看到底为啥只有一个条目的高度。看到这里

    final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);
    
    

    这里就是计算childHeight的地方,到这里我们就应该去了解一下MeasureSpec了.
    MeasureSpec: 从字面意思翻译像是测量规格,它在很大程度上决定了一个View的尺寸规则。实际上MeasureSpec 是一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。重点就在于这个SpecMode

    SpecMode: 测量模式有3种

    • UNSPECFIED: 父容器不对View有任何限制,要多大给多大。这种情况一般用于系统内部,表示一种测量状态。
    • EXACTLY: 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
    • AT_MOST: 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
    
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    /** @hide */
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}
    
    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    
    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    

    在了解了测量模式之后,在看这段代码

    final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);
    
    

    在这里ScrollView对子View的高的测量模式都制定成了MeasureSpec.UNSPECIFIED,也就是说,ListView要多高就给多高,这时候我们就可以去看看ListView到底要了多高。源码转入ListView,看看onMesaure()方法。

    @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;
    
        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);
        }
    
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }
    
        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;
    }
    
    

    看到这一段代码

    if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }
    
    

    当测量模式是MeasureSpec.UNSPECIFIED的时候,我们只看到了 heightSize = ** + childHeight **..,oh my god, 难道ListView只向他爸爸要了一个孩子的高度,这就可以解释清楚了为什么ScrollView嵌套ListView只显示一行的高度了。

    问题解决

    解决问题的方法在开头已经给出了,但是为什么要这么做呢。

    我们在观察ListView的onMesaure方法的时候发现,当heightMode == MeasureSpec.UNSPECIFIED时,ListView只测量了一个child的高度,下面看看当heightMode == MeasureSpec.AT_MOST的时候会发生什么。

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

    看这个方法貌似又去测量每个孩子的高度,然后赋值给heightSize了,看看方法的具体实现。

    /**
     * Measures the height of the given range of children (inclusive) and
     * returns the height with this ListView's padding and divider heights
     * included. If maxHeight is provided, the measuring will stop when the
     * current height reaches maxHeight.
     *
     * @return The height of this ListView with the given children.
     */
    final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
            int maxHeight, int disallowPartialChildPosition) {
        final ListAdapter adapter = mAdapter;
        if (adapter == null) {
            return mListPadding.top + mListPadding.bottom;
        }
    
        // Include the padding of the list
        int returnedHeight = mListPadding.top + mListPadding.bottom;
        final int dividerHeight = mDividerHeight;
        // The previous height value that was less than maxHeight and contained
        // no partial children
        int prevHeightWithoutPartialChild = 0;
        int i;
        View child;
    
        // mItemCount - 1 since endPosition parameter is inclusive
        endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
        final AbsListView.RecycleBin recycleBin = mRecycler;
        final boolean recyle = recycleOnMeasure();
        final boolean[] isScrap = mIsScrap;
    
        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);
    
            measureScrapChild(child, i, widthMeasureSpec, maxHeight);
    
            if (i > 0) {
                // Count the divider for all but one child
                returnedHeight += dividerHeight;
            }
    
            // Recycle the view before we possibly return from the method
            if (recyle && recycleBin.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                recycleBin.addScrapView(child, -1);
            }
    
            returnedHeight += child.getMeasuredHeight();
    
            if (returnedHeight >= maxHeight) {
                // We went over, figure out which height to return.  If returnedHeight > maxHeight,
                // then the i'th position did not fit completely.
                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                            && (i > disallowPartialChildPosition) // We've past the min pos
                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
                            && (returnedHeight != maxHeight) // i'th child did not fit completely
                        ? prevHeightWithoutPartialChild
                        : maxHeight;
            }
    
            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
                prevHeightWithoutPartialChild = returnedHeight;
            }
        }
    
        // At this point, we went through the range of children, and they each
        // completely fit, so return the returnedHeight
        return returnedHeight;
    }
    
    

    从注释我们就可以看出,这个方法return的是The height of this ListView with the given children. 也就是ListView包含孩子的高度。看代码中

    returnedHeight += child.getMeasuredHeight();
    

    returnHeight也是在不断的累加每个孩子的高度,所以最终会得到ListView的真实高度。

    所以,现在的问题就是如何指定ListView的MeasureSpec.SpecMode == MeasureSpec.AT_MOST
    再来看开头的解决方法

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

    makeMeasureSpec方法就能重新指定测量规则,第一个参数提供一个30位的SpecSize,第二个参数提供一个2位的SpecMode,然后该方法将其合并成一个新的MeasureSpec.

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    
    

    走完这个方法之后在调用父类的也就是ListView的onMeasure方法,重新获取测量模式和测量尺寸,这次拿到的测量模式就是AT_MOST,就可以去测量每个孩子的高度累加了。

    问题 为什么要使用Integer.MAX_VALUE

    这是整型的最大值,很大很大有32位,我们在上面 measureHeightOfChildren这个方法的时候,注意一段代码

    if (returnedHeight >= maxHeight) {
        // We went over, figure out which height to return.  If returnedHeight > maxHeight,
        // then the i'th position did not fit completely.
        return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                    && (i > disallowPartialChildPosition) // We've past the min pos
                    && (prevHeightWithoutPartialChild > 0) // We have a prev height
                    && (returnedHeight != maxHeight) // i'th child did not fit completely
                ? prevHeightWithoutPartialChild
                : maxHeight;
    }
    return returnedHeight;
    
    
    

    当returnedHeight >= maxHeight的时候,它会返回一个我们并不想要的值,我们需要的是returnedHeight,所以我们不能让这个判断成立,就选择了整型最大值。这个maxHeight追溯一下,就是我们设置的Integer.MAX_VALUE >> 2, 右移两位就剩下30位刚好和2位的模式配对。

    结束语

    此致 敬礼

    相关文章

      网友评论

        本文标题:从源码角度分析SrcollView嵌套ListView显示不全的

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