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

相關文章
相關標籤/搜索