1. 程式人生 > >Android View繪製原理解析

Android View繪製原理解析

概述
本篇文章主要講述View是如何在Android原始碼中產生的,以便於我們能夠更好的去自定義一些控制元件,大體上是按照View繪製的流程來走步驟,在追蹤原始碼之前我們先了解幾個基礎知識。來看下面的這張圖:
在這裡插入圖片描述
一張典型的系統View分解圖,一個Activity對應一個PhoneWindow、一個DecorView,DecorView是應用視窗的根部局,其本質是一個FrameLayout,有唯一的一個子View垂直的LinerLayout,包含兩個元素,TitleBar和ContentView,TitleBar大家都很熟悉,ContentView也是一個FrameLayout,它的子View就是在Activity元件中的OnCreate方法中setContentView(View)傳遞的View,這裡也能看出來Activity是和window(PhoneWindow是其實現類)是相關聯的,window是承載使用者介面的。

Window

Window就是視窗,這個概念是在Android的Framework中android.view.window這個抽象類,這個抽象類是對Android系統所有視窗的抽象,那麼什麼是視窗?
事實上視窗是一個巨集觀的概念,通常指螢幕上用於繪製各種UI元素以及響應使用者輸入事件的矩形區域,有兩個特點:

  1. 獨立繪製,不與其他介面相互影響
  2. 不會觸發其他介面的輸入事件
    在Android系統中,window視窗獨佔一個surface例項的顯示區域,每個視窗的surface由WindowManagerService來分配,我們可以將surface理解為一塊畫布,應用通過canvas或OpenGL在上面作畫,畫好之後將多塊surface通過surfaceFlinger按照特定的順序(即Z-order)進行混合,最後顯示在FrameBuffer中,這樣使用者介面就得以顯示出來。

而android.view.window這個抽象類可以視為Android系統對視窗這一巨集觀概念所做的約定,而PhoneWindow這個類是framework為我們提供的視窗具體實現,現在我們來看看window這個類
這個抽象類包括三個核心元件:

  1. WindowManager.LayoutParams:視窗的佈局引數
  2. CallBack:視窗的回撥介面,一般由Activity實現
  3. ViewTree:視窗所承載的view樹

接下來看看Android中window唯一的實現類—PhoneWindow

PhoneWindow

前面我們提到了,PhoneWindow這個類是Framework為我們提供的Android視窗的具體實現。我們平時呼叫setContentView()方法設定Activity的使用者介面時,實際上就完成了對所關聯的PhoneWindow的ViewTree的設定。我們還可以通過Activity類的requestWindowFeature()方法來定製Activity關聯PhoneWindow的外觀,這個方法實際上做的是把我們所請求的視窗外觀特性儲存到了PhoneWindow的mFeatures成員中,在視窗繪製階段生成外觀模板時,會根據mFeatures的值繪製特定外觀。

從setContentView()說開去

在分析setContentView()方法前,我們需要明確:這個方法只是完成了Activity的ContentView的建立,而並沒有執行View的繪製流程。
當我們自定義Activity繼承自android.app.Activity時候,呼叫的setContentView()方法是Activity類的,原始碼如下:

public void setContentView(@LayoutRes int layoutResID) {   
  getWindow().setContentView(layoutResID);   
  . . .
}

getWindow()方法會返回Activity所關聯的PhoneWindow,也就是說,實際上呼叫到了PhoneWindow的setContentView()方法,原始碼如下:

@Override
public void setContentView(int layoutResID) {
  if (mContentParent == null) {
    // mContentParent即為上面提到的ContentView的父容器,若為空則呼叫installDecor()生成
    installDecor();
  } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    // 具有FEATURE_CONTENT_TRANSITIONS特性表示開啟了Transition
    // mContentParent不為null,則移除decorView的所有子View
    mContentParent.removeAllViews();
  }
  if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    // 開啟了Transition,做相應的處理,我們不討論這種情況
    // 感興趣的同學可以參考原始碼
    . . .
  } else {
    // 一般情況會來到這裡,呼叫mLayoutInflater.inflate()方法來填充佈局
    // 填充佈局也就是把我們設定的ContentView加入到mContentParent中
    mLayoutInflater.inflate(layoutResID, mContentParent);
  }
  . . .
  // cb即為該Window所關聯的Activity
  final Callback cb = getCallback();
  if (cb != null && !isDestroyed()) {
    // 呼叫onContentChanged()回撥方法通知Activity視窗內容發生了改變
    cb.onContentChanged();
  }
 
  . . .
} 

LayoutInflater.inflate()

在上面我們看到了,PhoneWindow的setContentView()方法中呼叫了LayoutInflater的inflate()方法來填充佈局,這個方法的原始碼如下:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
  return inflate(resource, root, root != null);
}
 
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
  final Resources res = getContext().getResources();
  . . .
  final XmlResourceParser parser = res.getLayout(resource);
  try {
    return inflate(parser, root, attachToRoot);
  } finally {
    parser.close();
  }
}

在PhoneWindow的setContentView()方法中傳入了decorView作為LayoutInflater.inflate()的root引數,我們可以看到,通過層層呼叫,最終呼叫的是inflate(XmlPullParser, ViewGroup, boolean)方法來填充佈局。這個方法的原始碼如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  synchronized (mConstructorArgs) {
    . . .
    final Context inflaterContext = mContext;
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    Context lastContext = (Context) mConstructorArgs[0];
    mConstructorArgs[0] = inflaterContext;
 
    View result = root;
 
    try {
      // Look for the root node.
      int type;
      // 一直讀取xml檔案,直到遇到開始標記
      while ((type = parser.next()) != XmlPullParser.START_TAG &&
          type != XmlPullParser.END_DOCUMENT) {
        // Empty
       }
      // 最先遇到的不是開始標記,報錯
      if (type != XmlPullParser.START_TAG) {
        throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
      }
 
      final String name = parser.getName();
      . . .
      // 單獨處理<merge>標籤,不熟悉的同學請參考官方文件的說明
      if (TAG_MERGE.equals(name)) {
        // 若包含<merge>標籤,父容器(即root引數)不可為空且attachRoot須為true,否則報錯
        if (root == null || !attachToRoot) {
          throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
        }
 
        // 遞迴地填充佈局
        rInflate(parser, root, inflaterContext, attrs, false);
     } else {
        // temp為xml佈局檔案的根View
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        ViewGroup.LayoutParams params = null;
        if (root != null) {
          . . .
          // 獲取父容器的佈局引數(LayoutParams)
          params = root.generateLayoutParams(attrs);
          if (!attachToRoot) {
            // 若attachToRoot引數為false,則我們只會將父容器的佈局引數設定給根View
            temp.setLayoutParams(params);
          }
 
        }
 
        // 遞迴載入根View的所有子View
        rInflateChildren(parser, temp, attrs, true);
        . . .
 
        if (root != null && attachToRoot) {
          // 若父容器不為空且attachToRoot為true,則將父容器作為根View的父View包裹上來
          root.addView(temp, params);
        }
 
        // 若root為空或是attachToRoot為false,則以根View作為返回值
        if (root == null || !attachToRoot) {
           result = temp;
        }
      }
 
    } catch (XmlPullParserException e) {
      . . .
    } catch (Exception e) {
      . . .
    } finally {
 
      . . .
    }
    return result;
  }
}

在上面的原始碼中,首先對於佈局檔案中的標籤進行單獨處理,呼叫rInflate()方法來遞迴填充佈局。這個方法的原始碼如下:

void rInflate(XmlPullParser parser, View parent, Context context,
    AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 獲取當前標記的深度,根標記的深度為0
    final int depth = parser.getDepth();
    int type;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
      // 不是開始標記則繼續下一次迭代
      if (type != XmlPullParser.START_TAG) {
        continue;
      }
      final String name = parser.getName();
      // 對一些特殊標記做單獨處理
      if (TAG_REQUEST_FOCUS.equals(name)) {
        parseRequestFocus(parser, parent);
      } else if (TAG_TAG.equals(name)) {
        parseViewTag(parser, parent, attrs);
      } else if (TAG_INCLUDE.equals(name)) {
        if (parser.getDepth() == 0) {
          throw new InflateException("<include /> cannot be the root element");
        }
        // 對<include>做處理
        parseInclude(parser, context, parent, attrs);
      } else if (TAG_MERGE.equals(name)) {
        throw new InflateException("<merge /> must be the root element");
      } else {
        // 對一般標記的處理
        final View view = createViewFromTag(parent, name, context, attrs);
        final ViewGroup viewGroup = (ViewGroup) parent;
        final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
        // 遞迴地載入子View
        rInflateChildren(parser, view, attrs, true);
        viewGroup.addView(view, params);
      }
    }
 
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

我們可以看到,上面的inflate()和rInflate()方法中都呼叫了rInflateChildren()方法,這個方法的原始碼如下:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

從原始碼中我們可以知道,rInflateChildren()方法實際上呼叫了rInflate()方法。

到這裡,setContentView()的整體執行流程我們就分析完了,至此我們已經完成了Activity的ContentView的建立與設定工作。接下來,我們開始進入正題,分析View的繪製流程。

ViewRoot

在介紹View繪製之前,我們需要知道是誰負責執行View繪製的整體流程,實際上是由ViewRoot來負責繪製的,每個應用程式視窗的DecorView都有一個與之對應的ViewRoot物件,這種關聯關係是由WindowManager來管理的。
那麼DecorView和ViewRoot是何時相關聯的呢?是在ActivityThread類中handleMessage這個方法裡(命名為H的Handler的子類中的方法),在case Activity_Resume:呼叫了handleActivityResume這個方法繫結的,具體相關聯的時機和方式這裡按下不表,感興趣的話大家可以自己去翻原始碼,下面我們來分析ViewRoot如何繪製View

View繪製的起點

當建立好了DecorView和ViewRoot的聯絡之後,ViewRoot類的requestLayout方法就會被呼叫,,以完成應用程式使用者介面的初次佈局。實際被呼叫的是ViewRootImpl類的requestLayout()方法,這個方法的原始碼如下:

public void requestLayout() {
  if (!mHandlingLayoutInLayoutRequest) {
    // 檢查發起佈局請求的執行緒是否為主執行緒 
    checkThread();
    mLayoutRequested = true;
    scheduleTraversals();
  }
}

發起繪製的方法是scheduleTraversals(),這個方法會發送給主執行緒一個遍歷排程的"訊息",最終會呼叫ViewRootImpl.performTraversals方法,從這個方法開始,View會真正進入繪製的過程。

三個階段

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

  1. measure: 判斷是否需要重新計算View的大小,需要的話則計算;
  2. layout: 判斷是否需要重新計算View的位置,需要的話則計算;
  3. draw: 判斷是否需要重新繪製View,需要的話則重繪製
    這三個子階段可以用下圖來描述:
    在這裡插入圖片描述

1、measure

這個階段的目的是計算出控制元件樹中的各個控制元件要顯示的尺寸,measure的起點是MeasureHierarchy方法,方法原始碼如下:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res,
    final int desiredWindowWidth, final int desiredWindowHeight) {
  // 傳入的desiredWindowXxx為視窗尺寸
  int childWidthMeasureSpec;
  int childHeightMeasureSpec;
  boolean windowSizeMayChange = false;
  . . .
  boolean goodMeasure = false;
 
  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;
}

上面的方法使用getRootMeasureSpec方法獲取MeasureSpec,這個根MeasureSpec代表了對根DecorView視窗寬高的約束,在講述measure之前,我們需要先了解幾個測量的基礎知識點,之後的例項才能看懂
MeasureSpec
與view的measure方法打交道,一定會遇到MeasureSpec這個攔路虎,那麼MeasureSpec是什麼?翻譯過來就是測量引數或者是測量規格,官方文件對它的說明是:“一個MeasureSpec封裝了從父容器傳遞給子容器的佈局要求”,傳遞!!換句更準確的話說,這個MeasureSpec是(子view自身LayoutParams引數,其實就是我們在xml寫的時候設定的layout_width和layout_height 轉化而來的)和(父容器對子View的佈局約束MeasureSpec)通過簡單的計算得出的一個針對子View的測量要求,這個測量要求就是MeasureSpec。
大家都知道MeasureSpec是一個32位的整數,由SpecMode和SpecSize組成,前2位是mode,後30位是size,SpecMode有三種模式:

  1. MeasureSpec.EXACTLY:對子View提出了一個確切的建議尺寸(SpecSize);對應xml佈局檔案中寬高確切值或者寬高是match_parent
  2. MeasureSpec.AT_MOST:子View的大小不得超過SpecSize;對應xml佈局檔案中寬高wrap_content
  3. MeasureSpec.UNSPECIFIED: 對子View的尺寸不作限制,通常用於系統內部。
    傳入performMeasure()方法的MeasureSpec的SpecMode為EXACTLY,SpecSize為視窗尺寸。
    performMeasure()方法的原始碼如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
  . . .
  try {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  } finally {
    . . .
  }
}

當前我們是在測量根部局DecorView尺寸,上面的mView就是DecorView,而DecorView是一個FrameLayout,所以這個measure方法呼叫會轉向FrameLayout中的measure,這個方法的原始碼如下:

/**
 * 呼叫這個方法來算出一個View應該為多大。引數為父View對其寬高的約束資訊。
 * 實際的測量工作在onMeasure()方法中進行
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  . . .
  // 判斷是否需要重新佈局
 
  // 若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) {
    . . .
    // 先嚐試從緩從中獲取,若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);
      . . .
    }
    . . .
  }
  mOldWidthMeasureSpec = widthMeasureSpec;
  mOldHeightMeasureSpec = heightMeasureSpec;
  mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
      (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

從measure()方法的原始碼中我們可以知道,只有以下兩種情況之一,才會進行實際的測量工作:

forceLayout為true:這表示強制重新佈局,可以通過View.requestLayout()來實現;
needsLayout為true,這需要specChanged為true(表示本次傳入的MeasureSpec與上次傳入的不同),並且以下三個條件之一成立:
    sAlwaysRemeasureExactly為true: 該變數預設為false;
    isSpecExactly為false: 若父View對子View提出了精確的寬高約束,則該變數為true,否則為false
    matchesSpecSize為false: 表示父View的寬高尺寸要求與上次測量的結果不同

對於decorView來說,實際執行測量工作的是FrameLayout的onMeasure()方法,該方法的原始碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  int count = getChildCount();
  . . .
  int maxHeight = 0;
  int maxWidth = 0;
 
  int childState = 0;
  for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (mMeasureAllChildren || child.getVisibility() != GONE) {
      measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      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());
 
      . . .
    }
  }
 
  // Account for padding too
  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());
  }
 
  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec,
        childState << MEASURED_HEIGHT_STATE_SHIFT));
  . . .
}

FrameLayout是ViewGroup的子類,父View的測量過程是先去測量子View,遍歷測量完每一個子View後再去測量自己,通過getChildAt(i)取到每一個子View,然後上面的measureChildWithMargin方法就是去測量子View尺寸的,我們來分析一下是怎麼測量的,看原始碼:

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
 
// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最後都會封裝到這個個LayoutParams。
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
 
//根據父View的測量規格和父View自己的Padding,
//還有子View的Margin和已經用掉的空間大小(widthUsed),就能算出子View的MeasureSpec,具體計算過程看getChildMeasureSpec方法。
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); 
 
//通過父View的MeasureSpec和子View的自己LayoutParams的計算,算出子View的MeasureSpec,然後父容器傳遞給子容器的
// 然後讓子View用這個MeasureSpec(一個測量要求,比如不能超過多大)去測量自己,如果子View是ViewGroup 那還會遞迴往下測量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 
}
 
// spec引數   表示父View的MeasureSpec
// padding引數    父View的Padding+子View的Margin,父View的大小減去這些邊距,才能精確算出
//               子View的MeasureSpec的size
// childDimension引數  表示該子View內部LayoutParams屬性的值(lp.width或者lp.height)
//                    可以是wrap_content、match_parent、一個精確指(an exactly size), 
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
    int specMode = MeasureSpec.getMode(spec);  //獲得父View的mode 
    int specSize = MeasureSpec.getSize(spec);  //獲得父View的大小 
 
   //父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
    int size = Math.max(0, specSize - padding);  
 
    int resultSize = 0;    //初始化值,最後通過這個兩個值生成子View的MeasureSpec
    int resultMode = 0;    //初始化值,最後通過這個兩個值生成子View的MeasureSpec
 
    switch (specMode) { 
    // Parent has imposed an exact size on us 
    //1、父View是EXACTLY的 ! 
    case MeasureSpec.EXACTLY:  
        //1.1、子View的width或height是個精確值 (an exactly size) 
        if (childDimension >= 0) {           
            resultSize = childDimension;         //size為精確值 
            resultMode = MeasureSpec.EXACTLY;    //mode為 EXACTLY 。 
        }  
        //1.2、子View的width或height為 MATCH_PARENT/FILL_PARENT  
        else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size. So be it. 
            resultSize = size;                   //size為父檢視大小 
            resultMode = MeasureSpec.EXACTLY;    //mode為 EXACTLY 。 
        }  
        //1.3、子View的width或height為 WRAP_CONTENT 
        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;    //mode為AT_MOST 。 
        } 
        break; 
 
    // Parent has imposed a maximum size on us 
    //2、父View是AT_MOST的 !     
    case MeasureSpec.AT_MOST: 
        //2.1、子View的width或height是個精確值 (an exactly size) 
        if (childDimension >= 0) { 
            // Child wants a specific size... so be it 
            resultSize = childDimension;        //size為精確值 
            resultMode = MeasureSpec.EXACTLY;   //mode為 EXACTLY 。 
        } 
        //2.2、子View的width或height為 MATCH_PARENT/FILL_PARENT 
        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;                  //size為父檢視大小 
            resultMode = MeasureSpec.AT_MOST;   //mode為AT_MOST 
        } 
        //2.3、子View的width或height為 WRAP_CONTENT 
        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;   //mode為AT_MOST 
        } 
        break; 
 
    // Parent asked to see how big we want to be 
    //3、父View是UNSPECIFIED的 ! 
    case MeasureSpec.UNSPECIFIED: 
        //3.1、子View的width或height是個精確值 (an exactly size) 
        if (childDimension >= 0) { 
            // Child wants a specific size... let him have it 
            resultSize = childDimension;        //size為精確值 
            resultMode = MeasureSpec.EXACTLY;   //mode為 EXACTLY 
        } 
        //3.2、子View的width或height為 MATCH_PARENT/FILL_PARENT 
        else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size... find out how big it should 
            // be 
            resultSize = 0;                        //size為0! ,其值未定 
            resultMode = MeasureSpec.UNSPECIFIED;  //mode為 UNSPECIFIED 
        }  
        //3.3、子View的width或height為 WRAP_CONTENT 
        else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size.... find out how 
            // big it should be 
            resultSize = 0;                        //size為0! ,其值未定 
            resultMode = MeasureSpec.UNSPECIFIED;  //mode為 UNSPECIFIED 
        } 
        break; 
    } 
    //根據上面邏輯條件獲取的mode和size構建MeasureSpec物件。 
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
}

程式碼很多,但是原理並不複雜:
1、如果我們在xml 的layout_width或者layout_height 把值都寫死,那麼上述的測量完全就不需要了,之所以有上述的程式碼邏輯,是因為match_parent是父容器多大我(當前這個子View)就多大,wrap_content是自己實際多大就顯示多大,都沒有確切的定下來尺寸,開發者在xml佈局檔案中宣告這麼一個屬性非常容易,Android系統在默默的為我們實現了很多的功能,它要幫助我們計算出match_parent、wrap_content實際上是多大,這個計算過程呢,就是每一層計算出來的MeasureSpec不斷地往子View傳遞,然後結合子View的LayoutParams共同計算出子View的MeasureSpec,再遞迴傳遞給下層的子View,不斷計算每個View的MeasureSpec,子View有了MeasureSpec才能更測量自己和自己的子View。
2、上述程式碼如果這麼來理解就簡單了
如果父View的MeasureSpec 是EXACTLY,說明父View的大小是確切的,(確切的意思很好理解,如果一個View的MeasureSpec 是EXACTLY,那麼它的size 是多大,最後展示到螢幕就一定是那麼大)。
①、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是確切,子View的大小又MATCH_PARENT(充滿整個父View),那麼子View的大小肯定是確切的,而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY
②、如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根據自己的content 來決定的,但是子View的畢竟是子View,大小不能超過父View的大小,但是子View的是WRAP_CONTENT,我們還不知道具體子View的大小是多少,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 呼叫的時候才去真正測量子View 自己content的大小(比如TextView wrap_content 的時候你要測量TextView content 的大小,也就是字元佔用的大小,這個測量就是在child.measure(childWidthMeasureSpec, childHeightMeasureSpec)的時候,才能測出字元的大小,MeasureSpec 的意思就是假設你字元100px,但是MeasureSpec 要求最大的只能50px,這時候就要截掉了)。通過上述描述,子View MeasureSpec mode的應該是AT_MOST,而size 暫定父View的 size,表示的意思就是子View的大小沒有不確切的值,子View的大小最大為父View的大小,不能超過父View的大小(這就是AT_MOST 的意思),然後這個MeasureSpec 做為子View measure方法 的引數,做為子View的大小的約束或者說是要求,有了這個MeasureSpec子View再實現自己的測量。
③、如果如果子View 的layout_xxxx是確定的值(200dp),那麼就更簡單了,不管你父View的mode和size是什麼,我都寫死了就是200dp,那麼控制元件最後展示就是就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是這麼大,所以這種情況MeasureSpec 的mode = EXACTLY 大小size=你在layout_xxxx 填的那個值。
如果父View的MeasureSpec 是AT_MOST,說明父View的大小是不確定,最大的大小是MeasureSpec 的size值,不能超過這個值。
①、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不確定(只知道最大隻能多大),子View的大小MATCH_PARENT(充滿整個父View),那麼子View你即使充滿父容器,你的大小也是不確定的,父View自己都確定不了自己的大小,你MATCH_PARENT你的大小肯定也不能確定的,所以子View的mode=AT_MOST,size=父View的size,也就是你在佈局雖然寫的是MATCH_PARENT,但是由於你的父容器自己的大小不確定,導致子View的大小也不確定,只知道最大就是父View的大小。
②、如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不確定(只知道最大隻能多大),子View又是WRAP_CONTENT,那麼在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。
③、如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的。
如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示沒有任何束縛和約束,不像AT_MOST表示最大隻能多大,不也像EXACTLY表示父View確定的大小,子View可以得到任意想要的大小,不受約束
①、如果子View 的layout_xxxx是MATCH_PARENT,因為父View的MeasureSpec是UNSPECIFIED,父View自己的大小並沒有任何約束和要求,
那麼對於子View來說無論是WRAP_CONTENT還是MATCH_PARENT,子View也是沒有任何束縛的,想多大就多大,沒有不能超過多少的要求,一旦沒有任何要求和約束,size的值就沒有任何意義了,所以一般都直接設定成0
②、同上…
③、如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的(記住,只有設定的確切的值,那麼無論怎麼測量,大小都是不變的,都是你寫的那個值)
到此為止,我們應該對MeasureSpec 和三種模式、還有WRAP_CONTENT和MATCH_PARENT有一定的瞭解了
在measureChildWithMargins()方法中,獲取了知道子View測量的MeasureSpec後,接下來就要呼叫child.measure()方法,並把獲取到的childMeasureSpec傳入。這時便又會呼叫onMeasure()方法,若此時的子View為ViewGroup的子類,便會呼叫相應容器類的onMeasure()方法,其他容器View的onMeasure()方法與FrameLayout的onMeasure()方法執行過程相似。

下面會我們回到FrameLayout的onMeasure()方法,當遞迴地執行完所有子View的測量工作後,會呼叫resolveSizeAndState()方法來根據之前的測量結果確定最終對FrameLayout的測量結果並存儲起來。View類的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;
  switch (specMode) {
    case MeasureSpec.AT_MOST:
      if (specSize < size) {
        // 父View給定的最大尺寸小於完全顯示內容所需尺寸
        // 則在測量結果上加上MEASURED_STATE_TOO_SMALL
        result = specSize | MEASURED_STATE_TOO_SMALL;
      } else {
       result = size;
      }
      break;
 
    case MeasureSpec.EXACTLY:
      // 若specMode為EXACTLY,則不考慮size,result直接賦值為specSize
      result = specSize;
      break;
 
    case MeasureSpec.UNSPECIFIED:
    default:
      result = size;
  }
 
  return result | (childMeasuredState & MEASURED_STATE_MASK);
 
}

如果是View的測量過程主要是在onMeasure()方法

開啟View的原始碼,找到measure方法,這個方法程式碼不少,但是測量工作都是在onMeasure()做的,measure方法是final的所以這個方法也不可重寫,如果想自定義View的測量,你應該去重寫onMeasure()方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ......
  onMeasure(widthMeasureSpec,heightMeasureSpec);
  .....
}

View的onMeasure 的預設實現

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

View的onMeasure方法預設實現很簡單,就是呼叫setMeasuredDimension(),setMeasuredDimension()可以簡單理解就是給mMeasuredWidth和mMeasuredHeight設值,如果這兩個值一旦設定了,那麼意味著對於這個View的測量結束了,這個View的寬高已經有測量的結果出來了。如果我們想設定某個View的高寬,完全可以直接通過setMeasuredDimension(100,200)來設定死它的高寬(不建議),但是setMeasuredDimension方法必須在onMeasure方法中呼叫,不然會拋異常。對於普通View(非ViewgGroup)來說,只需完成自身的測量工作即可。以上程式碼中通過setMeasuredDimension()方法設定測量的結果,具體來說是以getDefaultSize()方法的返回值來作為測量結果。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;
}

看上述的程式碼,對於MeasureSpec的mode,AT_MOST和EXACTLY返回的是相同的都是specSize,所以當我們在xml佈局檔案中設定View的寬高為wrap_content時,與設定成match_parent是一樣的,都是充滿父佈局,getDefaultSize的第一個引數size等於getSuggestedMinimumXXXX返回的的值(建議的最小寬度和高度),而建議的最小寬度和高度都是由View的Background尺寸與通過設定View的minXXX屬性共同決定的,這個size可以理解為View的預設長度,而第二個引數measureSpec,是父View傳給自己的MeasureSpec,這個measureSpec是通過測量計算出來的,具體的計算測量過程前面在講解MeasureSpec已經講得比較清楚了(是有父View的MeasureSpec和子View自己的LayoutParams 共同決定的)只要這個測試的mode不是UNSPECIFIED(未確定的),那麼預設的就會用這個測量的數值當做View的高度。

對於View預設是測量很簡單,大部分情況就是拿計算出來的MeasureSpec的size 當做最終測量的大小。而對於其他的一些View的派生類,如TextView、Button、ImageView等,它們的onMeasure方法系統了都做了重寫,不會這麼簡單直接拿 MeasureSpec 的size來當大小,而去會先去測量字元或者圖片的高度等,然後拿到View本身content這個高度(字元高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那麼可以直接用View本身content的高度(字元高度等),而不是像View.java 直接用MeasureSpec的size做為View的大小。

--------------------------------------------這是一條華麗的分割線--------------------------------------------
下面我們通過一個xml佈局的例子來說明一個佈局檔案到底是怎麼測量出來的,先來看ViewTree的圖片
在這裡插入圖片描述
上面的圖片用xml佈局檔案寫出來是這樣的:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"   
   android:id="@+id/linear"
   android:layout_width="match_parent"   
   android:layout_height="wrap_content"   
   android:layout_marginTop="50dp"   
   android:background="@android:color/holo_blue_dark"   
   android:paddingBottom="70dp"   
   android:orientation="vertical">   
   <TextView       
    android:id="@+id/text"      
    android:layout_width="match_parent"    
    android:layout_height="wrap_content" 
    android:background="@color/material_blue_grey_800"      
    android:text="TextView"       
    android:textColor="@android:color/white"       
    android:textSize="20sp" />   
   <View      
      android:id="@+id/view"      
     android:layout_width="match_parent"
     android:layout_height="150dp"   
     android:background="@android:color/holo_green_dark" />
</LinearLayout>

整個圖是一個DecorView,DecorView可以理解成整個頁面的根View,DecorView是一個FrameLayout,包含兩個子View,一個id=statusBarBackground的View和一個是LineaLayout,id=statusBarBackground的View,我們可以先不管(我也不是特別懂這個View,應該就是statusBar的設定背景的一個控制元件,方便設定statusBar的背景),而這個LinearLayout比較重要,它包含一個title和一個content,title很好理解其實就是TitleBar或者ActionBar,content 就更簡單了,setContentView()方法你應該用過吧,android.R.id.content 你應該聽過吧,沒錯就是它,content是一個FrameLayout,你寫的頁面佈局通過setContentView加進來就成了content的直接子View。

整個View的佈局圖如下:
在這裡插入圖片描述
注:
1、 header的是個ViewStub,用來惰性載入ActionBar,為了便於分析整個測量過程,我把Theme設成NoActionBar,避免ActionBar 相關的measure干擾整個過程,這樣可以忽略掉ActionBar 的測量,在除錯程式碼更清晰。
2、包含Header(ActionBar)和id/content 的那個父View,我不知道叫什麼名字好,我們姑且叫它ViewRoot(看上圖),它是垂直的LinearLayout,放著整個頁面除statusBar 的之外所有的東西,叫它ViewRoot 應該還ok,一個代號而已。

這張圖在下面分析measure,會經常用到,主要用於瞭解遞迴的時候view 的measure順序
既然我們知道整個View的Root是DecorView,那麼View的繪製的入口是由ViewRootImpl的performTraversals方法來發起Measure,Layout,Draw等流程的。

我們來看下ViewRootImpl的performTraversals 方法:

private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
 
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;
   ......
  }
 return measureSpec;
} 

performTraversals 中我們看到的mView其實就是DecorView,View的繪製從DecorView開始, 在mView.measure()的時候呼叫getRootMeasureSpec獲得兩個MeasureSpec做為引數,getRootMeasureSpec的兩個引數(mWidth, lp.width)mWith和mHeight 是螢幕的寬度和高度, lp是WindowManager.LayoutParams,它的lp.width和lp.height的預設值是MATCH_PARENT,所以通過getRootMeasureSpec 生成的測量規格MeasureSpec 的mode是MATCH_PARENT ,size是螢幕的高寬。
因為DecorView 是一個FrameLayout 那麼接下來會進入FrameLayout 的measure方法,measure的兩個引數就是剛才getRootMeasureSpec的生成的兩個MeasureSpec,DecorView的測量開始了。
首先是DecorView 的 MeasureSpec ,根據上面的分析DecorView 的 MeasureSpec是Windows傳過來的,我們畫出DecorView 的MeasureSpec 圖:

在這裡插入圖片描述
注:
1、-1 代表的是EXACTLY,-2 是AT_MOST
2、由於螢幕的畫素是1440x2560,所以DecorView 的MeasureSpec的size 對應於這兩個值
那麼接下來在FrameLayout 的onMeasure()方法DecorView開始for迴圈測量自己的子View,測量完所有的子View再來測量自己,由下圖可知,接下來要測量ViewRoot的大小

//FrameLayout 的測量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
....
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {   
   final View child = getChildAt(i);   
   if (mMeasureAllChildren || child.getVisibility() != GONE) {  
    // 遍歷自己的子View,只要不是GONE的都會參與測量,measureChildWithMargins方法在最上面
    // 的原始碼已經講過了,如果忘了回頭去看看,基本思想就是父View把自己當MeasureSpec
    // 傳給子View結合子View自己的LayoutParams 算出子View 的MeasureSpec,然後繼續往下穿,
    // 傳遞葉子節點,葉子節點沒有子View,只要負責測量自己就好了。
     measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);     
     ....
     ....
   }
}
....
}

DecorView 測量ViewRoot 的時候把自己的widthMeasureSpec和heightMeasureSpec傳進去了,接下來要去看measureChildWithMargins的原始碼

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

ViewRoot 是系統的View,它的LayoutParams預設都是match_parent,根據我們文章最開始MeasureSpec 的計算規則,ViewRoot 的MeasureSpec mode應該等於EXACTLY(DecorView MeasureSpec 的mode是EXACTLY,ViewRoot的layoutparams 是match_parent),size 也等於DecorView的size,所以ViewRoot的MeasureSpec圖如下:
在這裡插入圖片描述
算出ViewRoot的MeasureSpec 之後,開始呼叫ViewRoot.measure 方法去測量ViewRoot的大小,然而ViewRoot是一個LinearLayout ,ViewRoot.measure最終會執行的LinearLayout 的onMeasure 方法,LinearLayout 的onMeasure 方法又開始逐個測量它的子View,上面的measureChildWithMargins方法又會被呼叫,那麼根據View的層級圖,接下來測量的是header(ViewStub),由於header的Gone,所以直接跳過不做測量工作,所以接下來輪到ViewRoot的第二個child content(android.R.id.content),我們要算出這個content 的MeasureSpec,所以又要拿ViewRoot 的MeasureSpec 和 android.R.id.content的LayoutParams 做計算了,計算過程就是呼叫getChildMeasureSpec的方法,
在這裡插入圖片描述

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
   .....
   final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,          
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed, lp.height); 
   ....
}
 
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
    int specMode = MeasureSpec.getMode(spec);  //獲得父View的mode 
    int specSize = MeasureSpec.getSize(spec);  //獲得父View的大小 
 
    int size = Math.max(0, specSize - padding); //父View的大小-自己的Padding+子View的Margin,得到值才是子View可能的最大值。 
     .....
} 

由上面的程式碼
int size = Math.max(0, specSize - padding);
而 padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed
算出android.R.id.content 的MeasureSpec 的size
由於ViewRoot 的mPaddingBottom=100px(這個可能和狀態列的高度有關,我們測量的最後會發現id/statusBarBackground的View的高度剛好等於100px,ViewRoot 是系統的View的它的Padding 我們沒法改變,所以計算出來Content(android.R.id.content) 的MeasureSpec 的高度少了100px ,它的寬高的mode 根據算出來也是EXACTLY(ViewRoot 是EXACTLY和android.R.id.content 是match_parent)。所以Content(android.R.id.content)的MeasureSpec 如下(高度少了100px):
在這裡插入圖片描述
Content(android.R.id.content) 是FrameLayout,遞迴呼叫開始準備計算id/linear的MeasureSpec,我們先給出結果:
在這裡插入圖片描述
圖中有兩個要注意的地方:
1、id/linear的heightMeasureSpec 的mode=AT_MOST,因為id/linear 的LayoutParams 的layout_height=“wrap_content”
2、id/linear的heightMeasureSpec 的size 少了200px, 由上面的程式碼
padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
int size = Math.max(0, specSize - padding);
由於id/linear 的 android:layout_marginTop=“50dp” 使得lp.topMargin=200px (本裝置的density=4,px=4*pd),在計算後id/linear的heightMeasureSpec 的size 少了200px。(佈局程式碼前面已給出,可自行檢視id/linear 控制元件xml中設定的屬性)
linear.measure接著往下算linear的子View的的MeasureSpec,看下View 層級圖,往下走應該是id/text,接下來是計算id/text的MeasureSpec,直接看圖,mode=AT_MOST ,size 少了280,別問我為什麼 …specSize - padding.
在這裡插入圖片描述
算出id/text 的MeasureSpec 後,接下來text.measure(childWidthMeasureSpec, childHeightMeasureSpec);準備測量id/text 的高寬,這時候已經到底了,id/text是TextView,已經沒有子類了,這時候跳到TextView的onMeasure方法了。TextView 拿著剛才計算出來的heightMeasureSpec(mode=AT_MOST,size=1980),這個就是對TextView的高度和寬度的約束,進到TextView 的onMeasure(widthMeasureSpec,heightMeasureSpec) 方法,在onMeasure 方法執行除錯過程中,我們發現下面的程式碼:
在這裡插入圖片描述
TextView字元的高度(也就是TextView的content高度[wrap_content])測出來=107px,107px 並沒有超過1980px(允許的最大高度),所以實際測量出來TextView的高度是107px。
最終算出id/text 的mMeasureWidth=1440px,mMeasureHeight=107px。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"   
   android:id="@+id/linear"
   android:layout_width="match_parent"   
   android:layout_height="wrap_content"   
   android:layout_marginTop="50dp"   
   android:background="@android:color/holo_blue_dark"   
   android:paddingBottom="70dp"   
   android:orientation="vertical">   
   <TextView       
    android:id="@+id/text"      
    android:layout_width="match_parent"    
    android:layout_height="wrap_content" 
    android:background="@color/material_blue_grey_800"      
    android:text="TextView"       
    android:textColor="@android:color/white"       
    android:textSize="20sp" />   
   <View      
      android:id="@+id/view"      
     android:layout_width="match_parent"
     android:layout_height="150dp"   
     android:background="@android:color/holo_green_dark" />
</LinearLayout>

TextView的高度已經測量出來了,接下來測量id/linear的第二個child(id/view),同樣的原理測出id/view的MeasureSpec.
在這裡插入圖片描述
d/view的MeasureSpec 計算出來後,呼叫view.measure(childWidthMeasureSpec, childHeightMeasureSpec)的測量id/view的高寬,之前已經說過View measure的預設實現是

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

最終算出id/view的mMeasureWidth=1440px,mMeasureHeight=600px。

id/linear 的子View的高度都計算完畢了,接下來id/linear就通過所有子View的測量結果計算自己的高寬,id/linear是LinearLayout,所有它的高度計算簡單理解就是子View的高度的累積+自己的Padding.
在這裡插入圖片描述
最終算出id/linear的mMeasureWidth=1440px,mMeasureHeight=987px。

最終算出id/linear出來後,id/content 就要根據它唯一的子View id/linear 的測量結果和自己的之前算出的MeasureSpec一起來測量自己的結果,具體計算的邏輯去看FrameLayout onMeasure 函式的計算過程。以此類推,接下來測量ViewRoot,然後再測量id/statusBarBackground,雖然不知道id/statusBarBackground 是什麼,但是除錯的過程中,測出的它的高度=100px, 和 id/content 的paddingTop 剛好相等。在最後測量DecorView 的高寬,最終整個測量過程結束。所有的View的大小測量完畢。所有的getMeasureWidth 和 getMeasureWidth 都已經有值了。Measure 分析到此為止

2、layout

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

performTraversals 方法執行完mView.measure 計算出mMeasuredXXX後就開始執行layout 函式來確定View具體放在哪個位置,我們計算出來的View目前只知道view矩陣的大小,具體這個矩陣放在哪裡,這就是layout 的工作了。layout的主要作用 :根據子檢視的大小以及佈局引數將View樹放到合適的位置上。

既然是通過mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 那我們來看下layout 函式做了什麼,mView肯定是個ViewGroup,不會是View,我們直接看下ViewGroup 的layout函式

public final void layout(int l, int t, int r, int b) {   
   if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {       
    if (mTransition != null) {           
       mTransition.layoutChange(this);       
    }      
    super.layout(l, t, r, b);   
    } else {       
    // record the fact that we noop'd it; request layout when transition finishes       
      mLayoutCalledWhileSuppressed = true;   
   }
}

程式碼可以看個大概,LayoutTransition用於處理增刪子View控制元件的動畫效果,我們不去管動畫,這個函式是final,不能被重寫的,所有的ViewGroup子類都會去呼叫它,layout的具體實現是super.layout(l,t,r,b),我們跳到View的layout原始碼中:

public final void layout(int l, int t, int r, int b) {
       .....
      //設定View位於父檢視的座標軸
       boolean changed = setFrame(l, t, r, b);
       //判斷View的位置是否發生過變化,看有必要進行重新layout嗎
       if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
           if (ViewDebug.TRACE_HIERARCHY) {
               ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
           }
           //呼叫onLayout(changed, l, t, r, b); 函式
           onLayout(changed, l, t, r, b);
           mPrivateFlags &= ~LAYOUT_REQUIRED;
       }
       mPrivateFlags &= ~FORCE_LAYOUT;
       .....
   }

1、setFrame(l, t, r, b) 可以理解為給mLeft 、mTop、mRight、mBottom賦值,然後基本就能確定View自己在父檢視的位置了,這幾個值構成的矩形區域就是該View顯示的位置,這裡的具體位置都是相對與父檢視的位置。

2、回撥onLayout,對於View來說,onLayout只是一個空實現,一般情況下我們也不需要過載該函式,而ViewGroup的onLayout是抽象方法,子類必須重寫實現它,而過載onLayout的目的就是安排其children在父檢視的具體位置,那麼如何安排子View的具體位置呢?

nt childCount = getChildCount() ;
  for(int i=0 ;i<childCount ;i++){
       View child = getChildAt(i) ;
       //整個layout()過程就是個遞迴過程
       child.layout(l, t, r, b) ;
    } 

程式碼很簡單,就是遍歷自己的孩子,然後呼叫 child.layout(l, t, r, b) ,給子view 通過setFrame(l, t, r, b) 確定位置,而重點是(l, t, r, b) 怎麼計算出來的呢。還記得我們之前測量過程,測量出來的MeasuredWidth和MeasuredHeight嗎?還記得你在xml 設定的Gravity嗎?還有RelativeLayout 的其他引數嗎,沒錯,就是這些引數和MeasuredHeight、MeasuredWidth 一起來確定子View在父檢視的具體位置的。具體的計算過程大家可以看下最簡單FrameLayout 的onLayout 函式的原始碼,每個不同的ViewGroup 的實現都不一樣,這邊不做具體分析了吧。
3、MeasuredWidth和MeasuredHeight這兩個引數為layout過程提供了一個很重要的依據(如果不知道View的大小,你怎麼固定四個點的位置呢),但是這兩個引數也不是必須的,layout過程中的4個引數l, t, r, b完全可以由我們任意指定,而View的最終的佈局位置和大小(mRight - mLeft=實際寬或者mBottom-mTop=實際高)完全由這4個引數決定,measure過程得到的mMeasuredWidth和mMeasuredHeight提供了檢視大小測量的值,但我們完全可以不使用這兩個值,所以measure過程並不是必須的。如果我們不使用這兩個值,那麼getMeasuredWidth() 和getWidth() 就很有可能不是同一個值,它們的計算是不一樣的:

public final int getMeasuredWidth() { 
        return mMeasuredWidth & MEASURED_SIZE_MASK; 
    } 
public final int getWidth() { 
        return mRight - mLeft; 
    }

layout 過程相對簡單些

3、draw

performTraversals 方法的下一步就是mView.draw(canvas); 因為View的draw 方法一般不去重寫,官網文件也建議不要去重寫draw 方法,所以下一步執行就是View.java的draw 方法,我們來看下原始碼:

public void draw(Canvas canvas) {
    ...
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
 
        // Step 1, draw the background, if needed
    ...
        background.draw(canvas);
    ...
        // skip step 2 & 5 if possible (common case)
    ...
        // Step 2, save the canvas' layers
    ...
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
 
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
    ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
 
        // Step 4, draw the children
        dispatchDraw(canvas);
 
        // Step 5, draw the fade effect and restore layers
 
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }
    ...
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

註釋寫得比較清楚,一共分成6步,看到註釋沒有( // skip step 2 & 5 if possible (common case))除了2 和 5之外 我們一步一步來看:
1、第一步:背景繪製
看註釋即可,不是重點

private void drawBackground(Canvas canvas) {
     Drawable final Drawable background = mBackground;
      ......
     //mRight - mLeft, mBottom - mTop layout確定的四個點來設定背景的繪製區域
     if (mBackgroundSizeChanged) {
        background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);  
        mBackgroundSizeChanged = false; rebuildOutline();
     }
     ......
     //呼叫Drawable的draw() 把背景圖片畫到畫布上
     background.draw(canvas);
     ......
}

第三步,對View的內容進行繪製。
onDraw(canvas) 方法是view用來draw 自己的,具體如何繪製,顏色線條什麼樣式就需要子View自己去實現,View.java 的onDraw(canvas) 是空實現,ViewGroup 也沒有實現,每個View的內容是各不相同的,所以需要由子類去實現具體邏輯。
第四步 對當前View的所有子View進行繪製
dispatchDraw(canvas)是用來繪製子View的,View.java 的dispatchDraw()方法是一個空方法,因為View沒有子View,不需要實現dispatchDraw ()方法,ViewGroup就不一樣了,它實現了dispatchDraw方法:

@Override
 protected void dispatchDraw(Canvas canvas) {
       ...
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
      ......
    }

程式碼一眼看出,就是遍歷子View然後drawChild(),drawChild()方法實際呼叫的是子View.draw()方法,ViewGroup類已經為我們實現繪製子View的預設過程,這個實現基本能滿足大部分需求,所以ViewGroup類的子類(LinearLayout,FrameLayout)也基本沒有去重寫dispatchDraw方法,我們在實現自定義控制元件,除非比較特別,不然一般也不需要去重寫它, drawChild()的核心過程就是為子檢視分配合適的cavas剪下區,剪下區的大小正是由layout過程決定的,而剪下區的位置取決於滾動值以及子檢視當前的動畫。設定完剪下區後就會呼叫子檢視的draw()函式進行具體的繪製了。
第六步 對View的滾動條進行繪製
不是重點,知道有這東西就行,onDrawScrollBars 的一句註釋 :Request the drawing of the horizontal and the vertical scrollbar. The scrollbars are painted only if they have been awakened first.

一張圖看下整個draw的遞迴流程
在這裡插入圖片描述

到此整個繪製過程基本講述完畢了。