AndroidQ FrameLayout的測量流程

上一篇文章總體分析了View的測量流程,從ViewRootImpl的performMeasure方法爲入口,以遞歸的方式從DecoreView開始測量,這篇文章來詳細分析一下FrameLayout遞歸測量完子View之後如何得到自己的size,比如對如下這個佈局文件的測量進行分析:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

我們根據FrameLayout的測量規則來分析這個佈局的測量流程

首先,我們來看看上面佈局中的FrameLayout的MeasureSpec怎麼生成的,上一篇文章中知道DecoreView在測量時會遍歷所有子View,並調用measureChildWithMargins方法,此方法主要作用通過getChildMeasureSpec生成子View的MeasureSpec,並調用子View的measure方法,最終調用的是子View實現的onMeasure方法,將生成的MeasureSpec傳遞過去

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

所以FrameLayout的onMeasure方法裏面接收的就是自己的MeasureSpec,我們把上一篇文章分析創建MeasureSpec的getChildMeasureSpec方法直接copy過來看看,因爲DecoreView的MeasureSpec的mode爲EXACTLY,size爲屏幕寬高,而且上面FrameLayout的xml佈局中寫的寬高是wrap_content,我們這裏忽略padding,margin,used,所以按照此方法的規則得到上面FrameLayout的MeasureSpec中的mode爲AT_MOST,size暫定爲父View的size即屏幕寬高

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //獲取父View的測量模式
        int specMode = MeasureSpec.getMode(spec);
        //獲取父View的size
        int specSize = MeasureSpec.getSize(spec);
        //由父View的size減去父View的padding和自己的margin以及Used
        //得到最終size(爲了方便後面依然統稱爲父View的size)
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
        //根據父View的測量模式以及自己LayoutParams計算MeasureSpec
        switch (specMode) {
        // 如果父View的模式爲EXACTLY
        case MeasureSpec.EXACTLY:
         //如果自己的xml中寫的值是一個不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //則自己的size就是xml中寫的值
                resultSize = childDimension;
                //自己的模式就是精確的
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中寫的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //則自己的size就是父View的size
                resultSize = size;
                //自己的模式就是精確的,因爲父View的模式是精確的
                resultMode = MeasureSpec.EXACTLY;
                //如果自己在xml中寫的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //則自己的size暫時定爲父View的size,因爲此時還沒有
                //對自己進行具體測量,無法得到具體值
                resultSize = size;
                //自己的模式爲AT_MOST,因爲就算還沒有具體測量但最終
                //肯定不能超過父View的大小
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父View的測量模式爲AT_MOST,即父View此時也不能確定自己的size
        case MeasureSpec.AT_MOST:
         //如果自己的xml中寫的值是一個不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //則自己的size就是xml中寫的值
                resultSize = childDimension;
                //自己的模式爲EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中寫的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //則自己的size就是父View的size
                resultSize = size;
                //自己的模式爲AT_MOST,因爲父View都不能確定自己大小
                resultMode = MeasureSpec.AT_MOST;
                //如果自己的xml中寫的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //則自己的size暫定爲父View的size
                resultSize = size;
                //自己的模式爲AT_MOST,自己雖然爲WRAP_CONTENT,
                //但最終還是不能超多父View大小
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //如果父View模式爲UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
         //如果自己的xml中寫的值是一個不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //則自己的size就是xml中寫的值
                resultSize = childDimension;
                //自己的模式爲EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中寫的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //則自己的size取決與sUseZeroUnspecifiedMeasureSpec,
                //這個值在View中初始化,當sdk版本小於M時爲true,大於爲false
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //自己的模式爲UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
                //如果自己的xml中寫的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //則自己的size取決與sUseZeroUnspecifiedMeasureSpec,
                //這個值在View中初始化,當sdk版本小於M時爲true,大於爲false
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //自己的模式爲UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //根據計算出來的size和mode生成自己的MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

FrameLayout的MeasureSpec已經計算出來了,mode爲AT_MOST,size爲暫定爲屏幕寬高,我們就接着分析它的onMeasure方法

FrameLayout.onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //獲取子View的個數,有兩個
        int count = getChildCount();
        //如果寬或者高的測量模式有一個不是EXACTLY就爲true
        //這什麼意思呢?其實是爲了後面對滿足條件的子View進行再次測量
        //因爲只要寬高有一個不是EXACTLY,就可能造成xml中有match_parent
        //的子View測量不準
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        //mMatchParentChildren裏裝的就是需要再次測量的子View
        //首先清空
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        //遍歷所有子View
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //對需要測量所有子View的情況,或者不爲Gone的view進行測量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
               //這個方法前面說過,作用是創建子View的MeasureSpec,並進行
               //測量
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                //獲取子View的LayoutParams,主要是爲了獲取xml中寫的值
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //這裏取FrameLayout子View中測量出來的寬度最大值
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                //這裏取FrameLayout子View中測量出來的高度最大值
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //前面說了這裏爲true
                if (measureMatchParentChildren) {
                    //對寬高有一個是MATCH_PARENT的子View都應該進行二次測量
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        //添加到mMatchParentChildren
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        //將子View計算出來的最大寬度再累加因爲Foreground造成
        //的左右padding
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        //將子View計算出來的最大高度再累加因爲Foreground造成
        //的上下padding
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        //不能小於最小高度
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        //不能小於最小寬度
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        
        final Drawable drawable = getForeground();
        //如果有背景圖片
        if (drawable != null) {
           //不能小於背景圖片的高
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            //不能小於背景圖片的寬
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
        //還需要調用resolveSizeAndState來對計算出來的最大寬高和
        //MeasureSpec中的size進行有條件取捨,才能得到最終寬高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        //省略後面部分代碼,後面一部分代碼是對需要二級測量的View進行測量
        //先來分析一下resolveSizeAndState方法
        .....
           ......
    }

我們再來看看最終調用setMeasuredDimension方法之前,會調用resolveSizeAndState方法再次對最終的大小進行選擇,這選擇是對MeasureSpec中的size和FrameLayout子View計算出來的最大size進行選擇,規則如下:

resolveSizeAndState

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
     
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        //根據FrameLayout的mode,得到對於的size
        switch (specMode) {
           //如果是AT_MOST
            case MeasureSpec.AT_MOST:
                //如果MeasureSpec中的size小於它計算出來的size
                if (specSize < size) {
                    //強制讓它等於MeasureSpec中的size,因爲模式已經是AT_MOST
                    //代表MeasureSpec的size是系統能給出的最大值了
                    //同時還會添加一個MEASURED_STATE_TOO_SMALL標記
                    //代表FrameLayout並沒有得到想要的大小
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    //在不超過最大值的情況下肯定會讓FrameLayout的大小
                    //等於計算出來的值
                    result = size;
                }
                break;
                //如果是精確模式,就等於MeasureSpec中的size
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

總結resolveSizeAndState方法規則:不考慮UNSPECIFIED情況,只有FrameLayout的mode爲AT_MOST並且計算出來的size大於它MeasureSpec的最大值情況下才不會滿足FrameLayout計算的值,因爲系統無法提供了,只能退而求其次選擇MeasureSpec中的size,而其他情況都會得到FrameLayout計算出來的值

然後調用setMeasuredDimension結束對FrameLayout的計算,FrameLayout的測量過程相對比較簡單,原理就是按它裏面的子View的最大寬高,再累加各種padding,並且需要滿足大於最小寬高的條件得出的size,得出size之後再和MeasureSpec中的size進行選擇得到最終的size

我們到現在只分析了FrameLayout的測量規則,但是並沒有分析如何根據子View寬高具體得到最終的FrameLayout的寬高

我們看下這個佈局,FrameLayout寬高爲wrap_content,如果它裏面都是寫死爲多少多少dp的View,那麼我們根據剛剛分析的測量規則一下就知道它的大小肯定等於它裏面最大的那個View寬高,但是這個佈局裏面有一個View寬高爲match_parent,那這種情況應該怎麼測量呢?

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

我們回到FrameLayout的onMeasure方法中看看它遍歷子View的測量代碼:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        for (int i = 0; i < count; i++) {
            ...
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ...
        }
   }

我們這裏對寬高爲match_parent的View的測量進行分析,measureChildWithMargins這個方法分析過多次了,它主要作用的創建子View的MeasureSpec,並調用measure進行測量

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

getChildMeasureSpec方法會根據父View的MeasureSpec和子View的LayoutParams計算出子View的MeasureSpec,我們忽略padding,margin,used,

父View爲FrameLayout,它的moder爲AT_MOST,size爲屏幕寬高,所以最終對應getChildMeasureSpec方法中如下分支:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        ...
        switch (specMode) {
        ...
        case MeasureSpec.AT_MOST:
            ...
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } 
            ...
            break;
          ....
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

即父View的模式爲AT_MOST,並且子View的xml中寫的是MATCH_PARENT,所以最終這個子View的size暫定爲父View MeasureSpec中的size,即手機屏幕寬高,模式爲AT_MOST,有了MeasureSpec之後接着調用它的measure方法進行測量,因爲我並沒有重寫View的onMeasure方法,所以最終會使用View默認的onMeasure方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

默認方法其實就是簡單的取MeasureSpec中的值,即這個寬高爲match_parent的View的最終寬高就是屏幕的寬高,所以最終這個wrap_content的FrameLayout寬高也就是屏幕寬高

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

還沒完,記得我們onMeasure方法中省略了部分代碼,是對必要的View進行二次測量:什麼是必要的View?就是我們前面也說過父View的模式不是EXACTLY,並且子View的寬高有一個是MATCH_PARENT的,因爲針對這種情況並不能一次性得到子View的寬高,雖然前面也得到了子View的寬高,但它的模式是AT_MOST,這並不是精確的,所以需要重新計算

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        ...
        for (int i = 0; i < count; i++) {
            ...
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        ...
        //二次計算的代碼
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

上面代碼看着多,其實就是分別對寬高進行二次測量,遍歷mMatchParentChildren中的View,mMatchParentChildren是存放需要二次測量的View的集合,
對裏面View分兩種情況重新測量,MATCH_PARENT和非MATCH_PARENT

 if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                }
                ...
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

我們看這個代碼多簡單,忽略padding,margin,其實就是通過getMeasuredWidth方法將View的MeasureSpec的AT_MOST模式改爲EXACTLY模式,最後再調用measure重新測量,其實重新測量也是之前的大小,只是mode變爲EXACTLY,說明xml中寫的是一個準確的值

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

所以最後我們可以得到寬高爲match_parent的View最終測量結果爲寬高等於屏幕大小,模式爲EXACTLY,而最外層的FrameLayout寬高爲wrap_content,根據FrameLayout測量規則得出是按最大子View算的,所以它的寬高也爲屏幕大小,模式爲AT_MOST,而寫了具體寬高的View測量得到的結果就是xml寫的,模式爲EXACTLY,最終一次FrameLayout完整的測量流程就完成了,結果如下圖:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章