ScrollView高度测量原理

       在使用Lint扫描工程时,看到这个提示。 Google推荐将ScrollView的子View高度设置为wrap_content,  但实际业务开发时可能根节点是LinearLayout(layout_height="match_parent"), 而后发现屏幕显不下就包了一层ScrollView。 运行看到ScrollView能正常上下滑动,就没改LinearLayout的layout_height属性。java

     为何ScrollView仍然能上下滑动呢???  按照安卓View的测量方式LinearLayout应该跟ScrollView的高度相同。 去源码里找答案:ScrollView重写了ViewGroup的measureChildWithMargins方法, 该方法会在onMeasure里调用。android

    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高度的测量方式为UNSPECIFIED, 这也是ScrollView子View高度参数无效的缘由。
        //UPSPECIFIED表示child高度由本身决定,不受父容器的限制
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
   核心是设置child高度测量方式为UNSPECIFIED,  这就是为何LinearLayout设置高度为match_parent仍然可以正常滑动的缘由。  后面再讲为何ScrollView要篡改子View高度的测量方式为UNSPECIFIED。less

     ScrollView和子View高度能够设置为wrap_content或者match_parent(与固定值高度状况相同)、 再考虑子View高度大于/小于ScrollView的高度,排列组合有8种状况。 上面说到给ScrollView的子View设置高度参数无效, 因此剩下4种状况。ide

 第一种状况:ScrollView高度是match_parent或固定值且子View高度小于ScrollView, 则子View高度是实际须要的高度。  若是须要子View高度等于父容器ScrollView, 则须要添加子View即LinearLayout属性android:fillViewPort="true"。函数

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //对应ScrollView的android:fillViewPort属性,默认值false
        if (!mFillViewport) {
            return;
        }
        //设置android:fillViewPort="true“后才会执行下面的代码
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            //没设置layout_height属性则返回
            return;
        }
 
        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            ... 
            final int desiredHeight = getMeasuredHeight() - heightPadding;
            if (child.getMeasuredHeight() < desiredHeight) {
                //若是ScrollView的子View高度小于本身则从新测量子View高度, 就是将ScrollView的高度赋值给子View高度。
                final int childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, widthPadding, lp.width);
                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredHeight, MeasureSpec.EXACTLY);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
第二种状况:ScrollView高度是match_parent或固定值且子View高度大于ScrollView,则测量后子View高度大于ScrollView高度。布局

 

第三种状况:ScrollView高度是wrap_content且子View高度低于屏幕高度, 则ScrollView和子View的高度相等, 即实际须要的大小。(比较好理解)ui

第四种状况:ScrollView高度是wrap_content且子View高度大于屏幕高度, 则ScrollView高度等于填满屏幕的高度, 而子View的高度大于ScrollView。 (后面会讲wrap_content的原理,解释这时ScrollView高度为何不等于子View)this

下面解释一下MeasureSpec是干吗的, 咱们知道View通过了onMeasure、onLayout、onDraw后才会显示出来。spa

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) .net

int类型占用4个字节即32比特, widthMeasureSpec和heightMeasureSpec的高2位是测量模式,低30位是在高2位的测量模式下获得的结果。 安卓有3种测量模式:

一、UNSPECIFIED: Measure specification mode: The parent has not imposed any constraint on the child. It can be whatever size it wants. 即父容器不限制本身的大小, 自定义ViewGroup才会配置该属性, 例如ScrollView。 通常用于framework。

二、EXACTLY: 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. 即父容器决定本身的大小, 对应将本身设置为match_parent或固定值。

三、AT_MOST:  Measure specification mode: The child can be as large as it wants up to the specified size. 即父容器指定了本身的最大值, 子View的大小不能超过specified size。 对应将本身设置为wrap_content.

例外:父容器wrap_content且子View是match_parent,则子View测量模式是AT_MOST.

     /* @param child The child to measure 待测量的子View
     * @param parentWidthMeasureSpec The width requirements for this view
     *        父容器(ViewGroup子类)宽的mesureSpec
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     *        父容器在水平方向已占用的大小(本身的兄弟View占用的空间)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     * 该方法的做用是肯定child的MeasureSpec
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //获得child的布局参数
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //获得child宽的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        //获得高的MeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //测量child宽、高
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
11111

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);  //父容器测量方式
        int specSize = MeasureSpec.getSize(spec);  //父容器测量的大小
        //padding是父容器在水平或垂直方向已占用的空间
        int size = Math.max(0, specSize - padding); //获得剩余可用大小
 
        int resultSize = 0;
        int resultMode = 0;
 
        //根据父容器的SpecModo肯定child的MeasureSpec
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY: //父容器是match_parent或固定值
            if (childDimension >= 0) {
                //若是child宽或高设置了固定值(例如10dp),则使用固定值做为specSize
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size; //使用父容器剩余空间做为specSize
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size; //父容器限制本身最大值是size(即剩余空间), 稍候用例子证实
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
 
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:   //父容器设置了wrap_content
            if (childDimension >= 0) { //例如layout_width="10dp"
                // Child wants a specific size... so be it
                resultSize = childDimension;  //child宽高设置了固定值
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size; //child的SpecSize不超过父容器
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size; //child的SpecSize不超过父容器
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
 
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:  //不限制子View
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;  //使用固定值
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; //根据布尔值设置specSize
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED; //wrap_content和match_parent逻辑相同
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
那么MeasureSpec最开始是如何跟wrap_content/match_parent/固定值关联上的呢???  在ViewRootImpl.java的getRootMesureSpec函数。 注意:Activity布局measure过程是从DecorView开始,测量模式为EXACTLY,宽高占满屏幕(即specSize等于屏幕的宽和高)。而后从DecorView逐级测量子View。 

/**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window 屏幕大小
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        //rootDimension是DecorView的参数,而DecorView配置的是match_parent. 测量模式是EXACTLY
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
  小结: 

一、若是宽/高设置了固定值(例如layout_width="10dp"), 那么MeasureSpec的specSize等于10dp,specMode是EXACTLY。

二、若是宽/高设置了wrap_content,   父容器是match_parent/wrap_content/固定值时本身的specSize不会超过父容器。

 三、若是宽/高设置了UPSPECIFIED,  本身想多大就多大,不受父容器的限制。 这就是为何ScrollView要篡改子View高度的测量方式为UPSPECIFIED的缘由。

   若是将ScrollView换成其它ViewGroup,能够看到Framework、LinearLayout的高度等于屏幕剩余高度, TextView高度是2000dp。

 

 

 

 

  ———————————————— 版权声明:本文为CSDN博主「brycegao321」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处连接及本声明。 原文连接:https://blog.csdn.net/brycegao321/article/details/87186309

相关文章
相关标签/搜索