1. 程式人生 > >Android View 的測量流程詳解

Android View 的測量流程詳解

概述

上一篇 Android DecorView 與 Activity 繫結原理分析 分析了在呼叫 setContentView 之後,DecorView 是如何與 activity 關聯在一起的,最後講到了 ViewRootImpl 開始繪製的邏輯。本文接著上篇,繼續往下講,開始分析 view 的繪製流程。

上文說到了呼叫 performTraversals 進行繪製,由於 performTraversals 方法比較長,看一個簡化版:

// ViewRootImpl 類
private void performTraversals() {
    // 這個方法程式碼非常多,但是重點就是執行這三個方法
    // 執行測量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // 執行佈局(ViewGroup)中才會有
    performLayout(lp, mWidth, mHeight);
    // 執行繪製
    performDraw();
}

其流程具體如下:

 View的整個繪製流程可以分為以下三個階段:

  • measure: 判斷是否需要重新計算 View 的大小,需要的話則計算;

  • layout: 判斷是否需要重新計算 View 的位置,需要的話則計算;

  • draw: 判斷是否需要重新繪製 View,需要的話則重繪製。

MeasureSpec

在介紹繪製前,先了解下 MeasureSpec。MeasureSpec 封裝了父佈局傳遞給子佈局的佈局要求,它通過一個 32 位 int 型別的值來表示,該值包含了兩種資訊,高兩位表示的是 SpecMode(測量模式),低 30 位表示的是 SpecSize

(測量的具體大小)。下面通過註釋的方式來分析來類:

/**  
 * 三種SpecMode: 
 * 1.UNSPECIFIED 
 * 父 ViewGroup 沒有對子View施加任何約束,子 view 可以是任意大小。這種情況比較少見,主要用於系統內部多次measure的情形,
* 用到的一般都是可以滾動的容器中的子view,比如ListView、GridView、RecyclerView中某些情況下的子view就是這種模式。
* 一般來說,我們不需要關注此模式。  * 2.EXACTLY   * 該 view 必須使用父 ViewGroup 給其指定的尺寸。對應 match_parent 或者具體數值(比如30dp)  * 3.AT_MOST   * 該 View 最大可以取父ViewGroup給其指定的尺寸。對應wrap_content  *    * MeasureSpec使用了二進位制去減少物件的分配。   */   public class MeasureSpec {           // 進位大小為2的30次方(int的大小為32位,所以進位30位就是要使用int的最高位和第二高位也就是32和31位做標誌位)           private static final int MODE_SHIFT = 30;           // 運算遮罩,0x3為16進位制,10進製為3,二進位制為11。3向左進位30,就是11 00000000000(11後跟30個0)           // (遮罩的作用是用1標註需要的值,0標註不要的值。因為1與任何數做與運算都得任何數,0與任何數做與運算都得0)           private static final int MODE_MASK  = 0x3 << MODE_SHIFT;           // 0向左進位30,就是00 00000000000(00後跟30個0)           public static final int UNSPECIFIED = 0 << MODE_SHIFT;           // 1向左進位30,就是01 00000000000(01後跟30個0)           public static final int EXACTLY     = 1 << MODE_SHIFT;           // 2向左進位30,就是10 00000000000(10後跟30個0)           public static final int AT_MOST     = 2 << MODE_SHIFT;           /**           * 根據提供的size和mode得到一個詳細的測量結果           */           // 第一個return:         // measureSpec = size + mode;   (注意:二進位制的加法,不是十進位制的加法!)           // 這裡設計的目的就是使用一個32位的二進位制數,32和31位代表了mode的值,後30位代表size的值           // 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100           //          // 第二個return:         // size &; ~MODE_MASK就是取size 的後30位,mode &amp; MODE_MASK就是取mode的前兩位,最後執行或運算,得出來的數字,前面2位包含代表mode,後面30位代表size         public static int makeMeasureSpec(int size, int mode) {               if (sUseBrokenMakeMeasureSpec) {                 return size + mode;             } else {                 return (size & ~MODE_MASK) | (mode &  MODE_MASK);             }         }           /**           * 獲得SpecMode          */           // mode = measureSpec &amp; MODE_MASK;           // MODE_MASK = 11 00000000000(11後跟30個0),原理是用MODE_MASK後30位的0替換掉measureSpec後30位中的1,再保留32和31位的mode值。           // 例如10 00..00100 & 11 00..00(11後跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值           public static int getMode(int measureSpec) {               return (measureSpec & MODE_MASK);           }           /**           * 獲得SpecSize           */           // size = measureSpec &  ~MODE_MASK;           // 原理同上,不過這次是將MODE_MASK取反,也就是變成了00 111111(00後跟30個1),將32,31替換成0也就是去掉mode,保留後30位的size           public static int getSize(int measureSpec) {               return (measureSpec &  ~MODE_MASK);           }   }  

順便提下 MATCH_PARENT 和 WRAP_CONTENT 這兩個代表的值,分別是 -1 和 -2。

       /**
         * Special value for the height or width requested by a View.
         * MATCH_PARENT means that the view wants to be as big as its parent,
         * minus the parent's padding, if any. Introduced in API Level 8.
         */
        public static final int MATCH_PARENT = -1;

        /**
         * Special value for the height or width requested by a View.
         * WRAP_CONTENT means that the view wants to be just large enough to fit
         * its own internal content, taking its own padding into account.
         */
        public static final int WRAP_CONTENT = -2;

Decorview 尺寸的確定

在 performTraversals 中,首先是要確定 DecorView 的尺寸。只有當 DecorView 尺寸確定了,其子 View 才可以知道自己能有多大。具體是如何去確定的,可以看下面的程式碼:

//Activity視窗的寬度和高度
int desiredWindowWidth;
int desiredWindowHeight;
...
//用來儲存視窗寬度和高度,來自於全域性變數mWinFrame,這個mWinFrame儲存了視窗最新尺寸
Rect frame = mWinFrame;
//構造方法裡mFirst賦值為true,意思是第一次執行遍歷嗎    
if (mFirst) {
    //是否需要重繪
    mFullRedrawNeeded = true;
    //是否需要重新確定Layout
    mLayoutRequested = true;
    
    // 這裡又包含兩種情況:是否包括狀態列
    
    //判斷要繪製的視窗是否包含狀態列,有就去掉,然後確定要繪製的Decorview的高度和寬度
    if (shouldUseDisplaySize(lp)) {
        // NOTE -- system code, won't try to do compat mode.
        Point size = new Point();
        mDisplay.getRealSize(size);
        desiredWindowWidth = size.x;
        desiredWindowHeight = size.y;
    } else {
        //寬度和高度為整個螢幕的值
        Configuration config = mContext.getResources().getConfiguration();
        desiredWindowWidth = dipToPx(config.screenWidthDp);
        desiredWindowHeight = dipToPx(config.screenHeightDp);
    }
    ...
 else{
    
        // 這是window的長和寬改變了的情況,需要對改變的進行資料記錄
    
        //如果不是第一次進來這個方法,它的當前寬度和高度就從之前的mWinFrame獲取
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        /**
         * mWidth和mHeight是由WindowManagerService服務計算出的視窗大小,
         * 如果這次測量的視窗大小與這兩個值不同,說明WMS單方面改變了視窗的尺寸
         */
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
            //需要進行完整的重繪以適應新的視窗尺寸
            mFullRedrawNeeded = true;
            //需要對控制元件樹進行重新佈局
            mLayoutRequested = true;
            //window視窗大小改變
            windowSizeMayChange = true;
        }
 }
    ...
    // 進行預測量
    if (layoutRequested){
        ...
        if (mFirst) {
            // 檢視視窗當前是否處於觸控模式。
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            //確保這個Window的觸控模式已經被設定
            ensureTouchModeLocally(mAddedTouchMode);
        } else {
            //六個if語句,判斷insects值和上一次比有什麼變化,不同的話就改變insetsChanged
            //insects值包括了一些螢幕需要預留的區域、記錄一些被遮擋的區域等資訊
            if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = true;
            }
            ...
            
          //  這裡有一種情況,我們在寫dialog時,會手動添加布局,當設定寬高為Wrap_content時,會把螢幕的寬高進行賦值,給出儘量長的寬度
            
            /**
             * 如果當前視窗的根佈局的width或height被指定為 WRAP_CONTENT 時,
             * 比如Dialog,那我們還是給它儘量大的長寬,這裡是將螢幕長寬賦值給它
             */
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = true;
                //判斷要繪製的視窗是否包含狀態列,有就去掉,然後確定要繪製的Decorview的高度和寬度
                if (shouldUseDisplaySize(lp)) {
                    // NOTE -- system code, won't try to do compat mode.
                    Point size = new Point();
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }
    }
}

這裡主要是分兩步走:

  1. 如果是第一次測量,那麼根據是否有狀態列,來確定是直接使用螢幕的高度,還是真正的顯示區高度。

  2. 如果不是第一次,那麼從 mWinFrame 獲取,並和之前儲存的長寬高進行比較,不相等的話就需要重新測量確定高度。

當確定了 DecorView 的具體尺寸之後,然後就會呼叫 measureHierarchy 來確定其 MeasureSpec :

 // Ask host how big it wants to be
  windowSizeMayChange |= measureHierarchy(host, lp, res,
        desiredWindowWidth, desiredWindowHeight); 

其中 host 就是 DecorView,lp 是 wm 在新增時候傳給 DecorView 的,最後兩個就是剛剛確定顯示寬高 ,看下方法的具體邏輯 :

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;boolean goodMeasure = false;
     // 說明是 dialog if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { // On large screens, we don't want to allow dialogs to just // stretch to fill the entire width of the screen to display // one line of text. First try doing the layout at a smaller // size to see if it will fit. final DisplayMetrics packageMetrics = res.getDisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); int baseSize = 0;
       // 獲取一個基本的尺寸 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize + ", desiredWindowWidth=" + desiredWindowWidth);
       // 如果大於基本尺寸 if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured (" + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec) + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
          // 判斷測量是否準確 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; } else { // Didn't fit in that size... try expanding a bit. baseSize = (baseSize+desiredWindowWidth)/2; if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize=" + baseSize); childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured (" + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")"); if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { if (DEBUG_DIALOG) Log.v(mTag, "Good!"); goodMeasure = true; } } } }      // 這裡就是一般 DecorView 會走的邏輯 if (!goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
       // 與之前的尺寸進行對比,看看是否相等,不想等,說明尺寸可能發生了變化 if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } return windowSizeMayChange; }

上面主要主要做的就是來確定父 View 的 MeasureSpec。但是分了兩種不同型別:

  • 如果寬是 WRAP_CONTENT 型別,說明這是 dialog,會有一些針對 dialog 的處理,最終會呼叫 performMeasure 進行測量;

  • 對於一般 Activity 的尺寸,會呼叫  getRootMeasureSpec MeasureSpec 。

下面看下 DecorView MeasureSpec 的計算方法:

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

 該方法主要是根據 View 的 MeasureSpec 是根據寬高的引數來劃分的。

  • MATCH_PARENT :精確模式,大小就是視窗的大小;

  • WRAP_CONTENT :最大模式,大小不定,但是不能超過視窗的大小;

  • 固定大小:精確模式,大小就是指定的具體寬高,比如100dp。

對於 DecorView 來說就是走第一個 case,到這裡 DecorView 的 MeasureSpec 就確定了,從 MeasureSpec 可以得出 DecorView 的寬高的約束資訊。

獲取子 view 的 MeasureSpec

當父 ViewGroup 對子 View 進行測量時,會呼叫 View 類的 measure 方法,這是一個 final 方法,無法被重寫。ViewGroup 會傳入自己的 widthMeasureSpec 和  heightMeasureSpec,分別表示父 View 對子 View 的寬度和高度的一些限制條件。尤其是當 ViewGroup 是 WRAP_CONTENT 的時候,需要優先測量子 View,只有子 View 寬高確定,ViewGroup 才能確定自己到底需要多大的寬高。

當 DecorView 的 MeasureSpec 確定以後,ViewRootImpl 內部會呼叫 performMeasure 方法:

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

該方法傳入的是對 DecorView 的 MeasureSpec,其中 mView 就是 DecorView 的例項,接下來看 measure() 的具體邏輯:

/**
 * 呼叫這個方法來算出一個View應該為多大。引數為父View對其寬高的約束資訊。
 * 實際的測量工作在onMeasure()方法中進行
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ......
  // Suppress sign extension for the low bytes
   long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
   if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
// 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制重新佈局
  // 比如呼叫View.requestLayout()會在mPrivateFlags中加入此標記
  final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
  final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
      || heightMeasureSpec != mOldHeightMeasureSpec;
  final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
      && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
  final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
      && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
  final boolean needsLayout = specChanged
      && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

  // 需要重新佈局  
  if (forceLayout || needsLayout) {
    // first clears the measured dimension flag 標記為未測量狀態
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
  // 對阿拉伯語、希伯來語等從右到左書寫、佈局的語言進行特殊處理
resolveRtlPropertiesIfNeeded();
// 先嚐試從緩從中獲取,若forceLayout為true或是快取中不存在或是
    // 忽略快取,則呼叫onMeasure()重新進行測量工作
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
      // measure ourselves, this should set the measured dimension flag back
      onMeasure(widthMeasureSpec, heightMeasureSpec);
      . . .
    } else {
      // 快取命中,直接從快取中取值即可,不必再測量
      long value = mMeasureCache.valueAt(cacheIndex);
      // Casting a long to int drops the high 32 bits, no mask needed
      setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
   // 如果自定義的View重寫了onMeasure方法,但是沒有呼叫setMeasuredDimension()方法就會在這裡丟擲錯誤;
   // flag not set, setMeasuredDimension() was not invoked, we raise
   // an exception to warn the developer
   if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
   throw new IllegalStateException("View with id " + getId() + ": "
  + getClass().getName() + "#onMeasure() did not set the"
  + " measured dimension by calling"
  + " setMeasuredDimension()");
   }

 

      //到了這裡,View已經測量完了並且將測量的結果儲存在View的mMeasuredWidth和mMeasuredHeight中,將標誌位置為可以layout的狀態

 

   mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
  }
  mOldWidthMeasureSpec = widthMeasureSpec;
  mOldHeightMeasureSpec = heightMeasureSpec;
// 儲存到快取中 mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }

 這裡要注意的是,這是一個 final 方法,不能被繼承。這個方法只在 View 類裡面。總結一下 measure() 都幹了什麼事:

  • 呼叫 View.measure()方法時 View 並不是立即就去測量,而是先判斷一下要不要進行測量操作,如果沒必要,那麼 View 就不需要重新測量了,避免浪費時間資源

  • 如果需要測量,在測量之前,會先判斷是否存在快取,存在直接從快取中獲取就可以了,再呼叫一下 setMeasuredDimensionRaw 方法,將從快取中讀到的測量結果儲存到成員變數 mMeasuredWidth 和 mMeasuredHeight 中。

  • 如果不能從 mMeasureCache 中讀到快取過的測量結果,呼叫 onMeasure() 方法去完成實際的測量工作,並且將尺寸限制條件 widthMeasureSpec 和 heightMeasureSpec 傳遞給 onMeasure() 方法。關於 onMeasure() 方法,會在下面詳細介紹。

  • 將結果儲存到 mMeasuredWidth 和 mMeasuredHeight 這兩個成員變數中,同時快取到成員變數 mMeasureCache 中,以便下次執行 measure() 方法時能夠從其中讀取快取值。

  • 需要說明的是,View 有一個成員變數 mPrivateFlags,用以儲存 View 的各種狀態位,在測量開始前,會將其設定為未測量狀態,在測量完成後會將其設定為已測量狀態。

DecorView 是 FrameLayout 子類,這時候應該去看 FrameLayout 中的 onMeasure() 方法,程式碼具體如下:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     // 獲取子view的個數 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);
       // mMeasureAllChildren 預設為FALSE,表示是否全部子 view 都要測量,子view不為GONE就要測量 if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 測量子view measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 獲取子view的佈局引數 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 記錄子view的最大寬度和高度 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());
// 記錄所有跟父佈局有著相同寬或高的子view if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } // Account for padding too 子view的最大寬高計算出來後,還要加上父View自身的padding maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); }      // 確定父 view 的寬高 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); 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;
          // 如果子view的寬是MATCH_PARENT,那麼寬度 = 父view的寬 - 父Padding - 子Margin 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); } } }

FrameLayout 是 ViewGroup 的子類,後者有一個 View[] 型別的成員變數 mChildren,代表了其子 View 集合。通過 getChildAt(i) 能獲取指定索引處的子 View,通過 getChildCount() 可以獲得子 View 的總數。

在上面的原始碼中,首先呼叫 measureChildWithMargins() 方法對所有子 View 進行了一遍測量,並計算出所有子View的最大寬度和最大高度。而後將得到的最大高度和寬度加上padding,這裡的padding包括了父View的padding和前景區域的padding。然後會檢查是否設定了最小寬高,並與其比較,將兩者中較大的設為最終的最大寬高。最後,若設定了前景影象,我們還要檢查前景影象的最小寬高。

經過了以上一系列步驟後,我們就得到了 maxHeight 和 maxWidth 的最終值,表示當前容器 View 用這個尺寸就能夠正常顯示其所有子View(同時考慮了 padding 和 margin )。而後我們需要呼叫 resolveSizeAndState() 方法來結合傳來的 MeasureSpec 來獲取最終的測量寬高,並儲存到 mMeasuredWidth 與 mMeasuredHeight 成員變數中。

如果存在一些子 View 的寬或高是 MATCH_PARENT,那麼需要等父 View 的尺寸計算出來後,再來決定這些子 view 的寬高。

下面看看 measureChildWithMargins() 方法具體邏輯:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @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)
     */
    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 的 MeasureSpec,然後呼叫 child.measure() 來完成子 View 的測量。下面看看子 View 獲取 MeasureSpec 的具體邏輯:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父 view 的 mode 和 size int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec);      // 去掉 padding int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; 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; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; 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; 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; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: 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; 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; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }

 方法清楚展示了普通 View 的 MeasureSpec 的建立規則,每個 View 的 MeasureSpec 狀態量由其直接父 View 的 MeasureSpec 和 View 自身的屬性 LayoutParams (LayoutParams 有寬高尺寸值等資訊)共同決定。

從上面的程式碼可以知道,返回 View 的 MeasureSpec 大致可以分為一下機制情況:
  • 子 View 為具體的寬/高,那麼 View 的 MeasureSpec 都為 LayoutParams 中大小。

  • 子 View 為 match_parent,父元素為精度模式(EXACTLY),那麼 View 的 MeasureSpec 也是精準模式他的大小不會超過父容器的剩餘空間。

  • 子 View 為 wrap_content,不管父元素是精準模式還是最大化模式(AT_MOST),View 的 MeasureSpec 總是為最大化模式並且大小不超過父容器的剩餘空間。

  • 父容器為 UNSPECIFIED 模式主要用於系統多次 Measure 的情形,一般我們不需要關心。

總結為下表:

 View.measure()  程式碼邏輯前面已經分析過了,最終會呼叫 onMeasuere 方法,下面看下 View.onMeasuere() 的程式碼:

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

上面方法中呼叫了 方法中呼叫了 setMeasuredDimension()方法,setMeasuredDimension()又呼叫了 getDefaultSize() 方法。getDefaultSize() 又呼叫了getSuggestedMinimumWidth()和 getSuggestedMinimumHeight(),那反向研究一下,先看下 getSuggestedMinimumWidth() 方法  (getSuggestedMinimumHeight() 原理 getSuggestedMinimumWidth() 跟一樣)。 

 protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

 原始碼很簡單,如果 View 沒有背景,就直接返回 View 本身的最小寬度 mMinWidth;如果給 View 設定了背景,就取 View 本身的最小寬度 mMinWidth 和背景的最小寬度的最大值.

那麼 mMinWidth 是哪裡來的?搜尋下原始碼就可以知道,View 的最小寬度 mMinWidth 可以有兩種方式進行設定:

  • 第一種是在 View 的構造方法中進行賦值的,View 通過讀取 XML 檔案中View設定的 minWidth 屬性來為 mMinWidth 賦值:
case R.styleable.View_minWidth:
     mMinWidth = a.getDimensionPixelSize(attr, 0);
     break;
  •  第二種是在呼叫 View 的 setMinimumWidth 方法為 mMinWidth 賦值
public void setMinimumWidth(int minWidth) {
    mMinWidth = minWidth;
    requestLayout();
}

 下面看下 getDefaultSize() 的程式碼邏輯:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

從註釋可以看出,getDefaultSize()這個測量方法並沒有適配 wrap_content 這一種佈局模式,只是簡單地將 wrap_content 跟 match_parent 等同起來。

到了這裡,我們要注意一個問題:

getDefaultSize()方法中 wrap_content 和 match_parent 屬性的效果是一樣的,而該方法是 View 的 onMeasure()中預設呼叫的,也就是說,對於一個直接繼承自 View 的自定義 View 來說,它的 wrap_content 和 match_parent 屬性是一樣的效果,因此如果要實現自定義 View 的 wrap_content,則要重寫 onMeasure() 方法,對 wrap_content 屬性進行處理。

如何處理呢?也很簡單,程式碼如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  //取得父ViewGroup指定的寬高測量模式和尺寸
  int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
  int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    //如果寬高都是AT_MOST的話,即都是wrap_content佈局模式,就用View自己想要的寬高值
        setMeasuredDimension(mWidth, mHeight);
  }else if (widthSpecMode == MeasureSpec.AT_MOST) {
    //如果只有寬度都是AT_MOST的話,即只有寬度是wrap_content佈局模式,寬度就用View自己想要的寬度值,高度就用父ViewGroup指定的高度值
        setMeasuredDimension(mWidth, heightSpecSize);
  }else if (heightSpecMode == MeasureSpec.AT_MOST) {
    //如果只有高度都是AT_MOST的話,即只有高度是wrap_content佈局模式,高度就用View自己想要的寬度值,寬度就用父ViewGroup指定的高度值
        setMeasuredDimension(widthSpecSize, mHeight);
  }
}

 在上面的程式碼中,我們要給 View 指定一個預設的內部寬/高(mWidth 和 mHeight),並在 wrap_content 時設定此寬/高即可。最後將在將寬高設定到 View 上:

    // View    
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
 
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

這裡就是把測量完的寬高值賦值給 mMeasuredWidthmMeasuredHeight 這兩個 View 的屬性,然後將標誌位置為已測量狀態。

子 View 測量完成以後,會計算 childState,看下 combineMeasuredStates 方法 :

public static int combineMeasuredStates(int curState, int newState) {
        return curState | newState;
    }

 當前 curState 為 0, newState 是呼叫 child.getMeasuredState() 方法得到的,來看下這個方法的具體邏輯:

 /**
     * Return only the state bits of {@link #getMeasuredWidthAndState()}
     * and {@link #getMeasuredHeightAndState()}, combined into one integer.
     * The width component is in the regular bits {@link #MEASURED_STATE_MASK}
     * and the height component is at the shifted bits
     * {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}.
     */
    public final int getMeasuredState() {
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }

 該方法返回一個 int 值,該值同時包含寬度的 state 以及高度的 state 資訊,不包含任何的尺寸資訊。

  • MEASURED_STATE_MASK 的值為 0xff000000,其高位元組的 8 位全部為 1,低位元組的 24 位全部為 0。

  • MEASURED_HEIGHT_STATE_SHIFT 值為 16。

  • 將 MEASURED_STATE_MASK 與 mMeasuredWidth 做與操作之後就取出了儲存在寬度首位元組中的 state 資訊,過濾掉低位三個位元組的尺寸資訊。

  • 由於 int 有四個位元組,首位元組已經存了寬度的 state 資訊,那麼高度的 state 資訊就不能存在首位位元組。MEASURED_STATE_MASK 向右移 16 位,變成了 0x0000ff00,這個值與高度值 mMeasuredHeight 做與操作就取出了 mMeasuredHeight 第三個位元組中的資訊。而 mMeasuredHeight 的 state 資訊是存在首位元組中,所以也得對mMeasuredHeight 向右移相同的位置,這樣就把 state 資訊移到了第三個位元組中。

  • 最後,將得到的寬度 state 與高度 state 按位或操作,這樣就拼接成一個 int 值,該值首個位元組儲存寬度的 state 資訊,第三個位元組儲存高度的 state 資訊。

這些都得到之後,就可以開始去計算父 View 的尺寸了:

        // 確定父 View 的寬高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

 下面開始看 resolveSizeAndState 具體邏輯:

// View 的靜態方法
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;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

這個方法的程式碼結構跟前文提到的 getDefaultSize()方法很相似,主要的區別在於 specMode 為 AT_MOST 的情況。我們當時說 getDefaultSize() 方法是沒有適配wrap_content 這種情況,而這個 resolveSizeAndState() 方法是已經適配了 wrap_content 的佈局方式,那具體怎麼實現 AT_MOST 測量邏輯的呢?有兩種情況:

  • 當父 ViewGroup 指定的最大尺寸比 View 想要的尺寸還要小時,會給這個父 ViewGroup  的指定的最大值 specSize 加入一個尺寸太小的標誌  MEASURED_STATE_TOO_SMALL,然後將這個帶有標誌的尺寸返回,父 ViewGroup 通過該標誌就可以知道分配給 View 的空間太小了,在視窗協商測量的時候會根據這個標誌位來做視窗大小的決策。

  • 當父 ViewGroup 指定的最大尺寸比沒有比 View 想要的尺寸小時(相等或者 View 想要的尺寸更小),直接取 View 想要的尺寸,然後返回該尺寸。

getDefaultSize() 方法只是 onMeasure() 方法中獲取最終尺寸的預設實現,其返回的資訊比 resolveSizeAndState() 要少,那麼什麼時候才會呼叫 resolveSizeAndState() 方法呢? 主要有兩種情況:

  • Android 中的大部分 ViewGroup 類都呼叫了 resolveSizeAndState() 方法,比如 LinearLayout 在測量過程中會呼叫 resolveSizeAndState() 方法而非 getDefaultSize()方法。

  • 我們自己在實現自定義的 View 或 ViewGroup 時,我們可以重寫 onMeasure() 方法,並在該方法內呼叫 resolveSizeAndState() 方法。

到此,終於把 View 測量過程講完了。

下一篇開始講 View 的 layout 和 draw 過程。

 

參考文章

Android原始碼完全解析——View的Measure過程

View的繪製流程