AndroidQ View的測量流程onMeasure

本篇文章來分析一下View的測量流程,在寫自定義控件時往往需要重寫View的onMeasure來定義自己的測量規矩,如果我們不熟悉測量流程,就算根據別人的自定義控件學習,最後寫出來了,但是再遇到新的自定義控件可能還是無從下手

我們都知道View的測量,佈局,繪製的入口在ViewRootImpl的performTraversals方法開始,而View的測量則是從最頂層的DecorView開始的,先測量子View,在測量父View,測量的規則其實就是根據父View的MeasureSpec和子View的LayoutParams再根據一個特定規則算出子View的大小

MeasureSpec

MeasureSpec中包含一個View的測量模式和size,這個size並不一定是最終View的大小

     int specMode = MeasureSpec.getMode(spec);
     int specSize = MeasureSpec.getSize(spec);

測量模式有三種:
UNSPECIFIED: 不限制View的大小,如ListView,scrollView,無法進行限制
EXACTLY: 精確模式,如在xml中寫死View的大小或者在父View模式爲EXACTLY時自己在xml中寫的match_parent
AT_MOST: 在xml中寫的wrap_content或者在父View模式爲AT_MOST時自己在xml中寫的match_parent

我們前面說了測量一個View是用的父View的MeasureSpec和子View的LayoutParams來計算的,那麼最頂層的DecorView沒有父View,它的MeasureSpec哪來的呢?

我們從測量的入口來看看DecorView的MeasureSpec的獲取

performTraversals

private void performTraversals() {
	  ......
	  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	  ......

}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {

        }
    }

在ViewRootImpl的performTraversals方法中調用performMeasure開始測量,performMeasure中調用mView的measure方法,這裏的mView就是DecorView,我們來看看childWidthMeasureSpec和childHeightMeasureSpec

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

childWidthMeasureSpec和childHeightMeasureSpec的獲取是通過調用getRootMeasureSpec方法,傳遞了mWidth,mHeight以及lp.width,lp.height,lp是在ViewRootImpl中創建的LayoutParams,調用了WindowManager.LayoutParams()的無參構造函數,默認值是:MATCH_PARENT

public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        }

而mWidth和mHeight則是手機屏幕的寬高,再看看getRootMeasureSpec方法

 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

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

getRootMeasureSpec方法中合成了DecoreView的MeasureSpec,根據傳遞的lp.width,lp.height爲MATCH_PARENT得到傳遞到DecorView的MeasureSpec模式爲EXACTLY,size爲手機屏幕的寬高,接着看performMeasure中調用的mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

DecoreView是一個FrameLayout,所以會調用FrameLayout的measure方法,而measure方法定義在View中,是final修飾的,所以子類無法繼承,實際上View的measure方法中調用了onMeasure,這個方法是給子類實現自己的測量規則的

FrameLayout.onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		......
	     for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //mMeasureAllChildren代表是否測量所有View
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                 ...
                }
            }
		......
}

遍歷DecoreView中的所有子View,如果mMeasureAllChildren爲true或者當前子View不是Gone狀態則進一步調用measureChildWithMargins測量子View,到這裏我們先說一下DecoreView的內部佈局,DecoreView內部會在PhoneWindow的setContentView方法中創建DecoreView時加載一個如下xml佈局,不考慮任何樣式的情況下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

所以可以簡單認爲DecoreView中包含一個LinearLayout,這個LinearLayout中包含一個actionbar和一個id爲content的FrameLayout,通過setContentView填充的就是這個FrameLayout,忽略actionbar的測量

好了,瞭解了DecoreView內部佈局後我們再回到其onMeasure方法中,遍歷DecoreView的子View,調用measureChildWithMargins開始測量,這裏子View就是這個LinearLayout

measureChildWithMargins

此方法定義在ViewGroup中

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //(1)
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //(2)
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        //(3)
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //(4)
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

我們分幾步來看看,首先:
(1)獲取LinearLayout的LayoutParams,獲取LayoutParams的目的其實是需要獲取xml中定義的Margin以及layout_width,layout_height

(2)(3)分別計算LinearLayout寬高的MeasureSpec,計算MeasureSpec時需要父View的MeasureSpec和父View的Padding和自己的Margin再加上一個widthUsed或者heightUsed,被其他View使用過的空間

(4)得到了LinearLayout的MeasureSpec之後會傳遞給內部的子類,子類再進行測量

getChildMeasureSpec

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

getChildMeasureSpec這個方法看着很多代碼,其實仔細看,邏輯很清晰,只要記住自己的MeasureSpec一定是根據父View的MeasureSpec和自己的LayoutParams來計算的,是兩個條件共同決定,所以有很多判斷,大致對這些判斷進行分類其實有三個大分支:

  1. 父View的測量模式爲EXACTLY時
    子View只有在xml中寫了WRAP_CONTENT時才需要在自己onMeasure方 法中定義測量規則
  2. 父View的測量模式爲AT_MOST時
    子View在xml寫了WRAP_CONTENT和MATCH_PARENT時都需要在自己onMeasure方法中定義測量規則
  3. 父View的測量模式爲UNSPECIFIED時
    不用管,這一般是系統的View

總結:View需要測量的情況其實就是自己的mode計算出來爲AT_MOST時,如果是EXACTLY的模式,根本不需要我們定義測量規則

好了由DecoreView的MeasureSpec就計算出了它裏面LinearLayout的MeasureSpec了,這個LinearLayout的layout_width和layout_height是match_parent,根據上面分析的測量規則,所以這個LinearLayout的mode就是EXACTLY,size爲DecoreView的size減去DecoreView內部padding和自己的margin

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

得到了MeasureSpec之後,measureChildWithMargins方法的(4)步會調用LinearLayout的measure方法繼續進行測量,前面已經說過measure這個方法定義在View中,不可繼承,實際上是在measure中調用子類onMeasure方法

LinearLayout.onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

這裏分爲垂直佈局和水平佈局,隨便看一個

measureHorizontal

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
     for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            //useExcessSpace代表是否使用權重的計算方式
            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
               ...
            } else {
                ...
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);        
   }
}

同樣是對LinearLayout內部的View進行遍歷,如果不是採用權重的方式計算則調用measureChildBeforeLayout方法測量LinearLayout的子View,measureChildBeforeLayout方法中同樣是調用了我們前面分析的measureChildWithMargins來測量,傳遞的是LinearLayout的MeasureSpec,我們可以發現其實是按一種遞歸的方法,從最頂層的DecoreView到LinearLayout到裏面的子View,如果繼續還是ViewGroup,可以想到,肯定還是遍歷其子View調用measureChildWithMargins,直到最終找到非ViewGroup的View,調用其onMeasure方法,此時的onMeasure肯定就不會遍歷子View了,而是實實在在的測量自己,當所以子View測量完成之後,再對那些xml中寫的是WRAP_CONTENT的父View進行進一步測量,所以我們前面說measureChildWithMargins測量的size並不一定就是最終View的size,
然後按不同的規則,比如LinearLayout可能是子View的寬或者高累加,FrameLayout可能是按它裏面最大的View的寬高得到的,這就需要去看它的源碼如何定義的規則

我們在自定義View時,重寫onMeasure方法時,它裏面的widthMeasureSpec和heightMeasureSpec其實就是通過ViewRootImpl的performMeasure方法開始,從DecoreView開始測量,計算的MeasureSpec一步一步,慢慢計算傳遞過來的

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

說白了,測量流程其實就是爲了給那些mode爲AT_MOST的自定義View規定一個測量規則好計算自己的size,mode爲AT_MOST時並不是一個準確的值,只是暫時的值,所以需要你重寫onMeasure方法,告訴系統,你的View的測量規則,對於那些自定義View沒有重寫onMeasure方法的View,Android提供了一個默認的測量方法,我們來看下

View.onMeasure

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         //設置View的寬高,此方法調了之後,測量流程就結束了
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

先看看getDefaultSize方法

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //獲取自己的測量模式
        int specMode = MeasureSpec.getMode(measureSpec);
        //獲取自己MeasureSpec的size
        int specSize = MeasureSpec.getSize(measureSpec);
        /*
           只要自己的模式不爲UNSPECIFIED,則自己的寬高就等於
           MeasureSpec保存的寬高
        */
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

所以對一般的自定義View來說,如果你不重寫onMeasure方法就默認使用MeasureSpec的size,而MeasureSpec的size根據我們前面getChildMeasureSpec方法的分析,不重寫onMeasure情況下,WRAP_CONTENT等價於MATCH_PARENT

總結一下View的測量過程:

  1. View的測量是從ViewRootImpl的performMeasure爲入口,首先會根據手機屏幕的寬高和默認值MATCH_PARENT給DecoreView創建一個MeasureSpec
  2. 接着調用DecoreView的measure實際調用FrameLayout的onMeasure方法進行測量,onMeasure中會遍歷子類View,以此調用measureChildWithMargins測量子View
  3. 接着又會調用id爲content的FrameLayout的onMeasure方法測量其子View,所以View的測量其實就是一個遞歸的過程
  4. measureChildWithMargins測量子View其實就是根據父View的MeasureSpec和子View自己的LayoutParams計算得到子View自己的MeasureSpec,這裏麪包含了子View的測量模式和暫時的size
  5. 自定義View或者自定義ViewGroup時一定要重寫onMeasure方法,只要你打算在xml中使用WRAP_CONTENT

分析完整個View測量流程之後我們再來聊聊爲什麼Activity的onCreate方法中獲取不到View的寬高,我們能想到要獲取View寬高一定要等View測量完成纔行,畢竟沒有測量完誰知道大小呢,所以我們能夠進一步想到onCreate方法一定是在View的測量之前執行的,就是這個原因,Activity執行onCreate回調時View的測量還沒開始呢

View的測量是在Activity的onResume回調纔開始執行的

後面會再寫一篇UI刷新機制詳細分析View測量流程是怎麼開始的

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