第八章 Android 中View的工作原理

??Android中的View在Android的知識體系中扮演著重要的角色。簡單來說,View就是Android在視覺的體現。我們所展現的頁面就是Android提供的GUI庫中控件的組合。但是當要求不能滿足于控件的時候,我們就需要自定義控件/自定義View來滿足我們的要求。為了掌握自定義View,我們需要了解View的底層工作原理,了解View的測量流程,布局流程以及繪制流程,還有View常見的回調方法,比如構造方法,onAttach,onVisibilityChanged,onDetach等,以及滑動效果處理。

1. View的繪制流程

??View的繪制流程主要就是measure,layout,draw這三個流程,對應的View的方法是onMeasure(),onLayout(),onDraw()這三個方法,即是測量,布局,繪制三個流程。其中measure 是測量View的寬高,layout確定View的布局內容,設置四個頂點,長寬,最后通過draw將View的內容繪制到屏幕上。

??在View繪制前,我們了解下Activity中的onCreate的中執行setContentView()方法之后View是顯示在屏幕上的,這個過程就不分析了,輔助我們理解整個流程。當調用Activity的setContentView()方法之后,會調用PhoneView類的setContentView方法,PhoneView類是抽象類Window的實現類,Window 類用來描述 Activity 視圖最頂端的窗口顯示和行為操作,PhoneView類的setContentView方法中最終會生成DecorView對象,DecorView是PhoneView類的內部類。下面是DecorView的機構圖可以參考一下,需要進入到View層,就需要先進入到DecorView層。


DecorView的結構圖

??ViewRoot 對應的實現類是 ViewRootImpl 類,他是連接 WindowManager 和DecorView 的紐帶,View 的三大 流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 activity 對象被創建完畢后,會將 DecorView 添加到Window 中,同時會創建 ViewRootImpl 對象,并將 ViewRootImpl 對象和 DecorView 建立關聯,這個過程參見源碼。

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparms,panelParentView);
DecorView添加窗口到View的過程

??View的繪制流程就是從ViewRoot的performTraversals方法開始的,它經過measuer,layout和draw三個過程才能最終將一個View繪制出來,針對performTraversals的大致流程。如下圖所示:


performTraversals的工作流程

??如圖所示,performTraversals會一次調用performMeasure,performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure,layout和draw這三個流程。其中performMeasure會調用measure方法,measure方法又會調用onMeasure方法,在onMeasure方法中則會對所有的子元素進行measure過程,這時候measure流程就從父元素傳遞到了子元素,這樣就完成了一次measure的過程。接著子元素會重復父元素measure的過程,如此反復就完成了父元素View的遍歷。同理performLayout和perforDraw也是同樣的道理。唯一不同的是,performDraw的傳遞過程是draw方法中的dispatchDraw方法來實現,本質上原理都是一致的。

??meaure過程決定了view的寬高。我們可以通過getMeasureWidth和getMeasureHeight方法來獲取View的寬高。幾乎在所有的情況下,這個寬高就是View的最終寬高。layout過程決定了view的四個點的坐標和實際的寬高。可以通過getTop,getRight, getLeft,getBottom來得到四個點的坐標,通過getWidth和getHeight得到實際的寬高。draw的過程就是顯示的過程,只有在draw完成之后才能最終顯示在屏幕上。

??DecorView 其實是一個 FrameLayout,View層事件都先經過DecorView ,然后才傳給View。

2. 理解MeasureSpec

??MeasureSpec,測量規格,測量說明,從名字上看起來都知道它的作用就是決定View的測量過程或者說它在很大程度上決定了View的尺寸規格。除此之外還有父容器也會影響View的創建過程。在測量過程中,系統會將View的LayoutParams根據父容器的規則轉換成對應的MeasureSpex,然后再根據這個MeasureSpec來測量View的寬高。

??MeasureSpec代表了一個32位的int的值,高2位代表了SpecMode,低30位代表了SpecSize。SpecMode指測量模式,SpecSize是指在某種測量模式下的規格大小。MeasureSpec 通過將 SpecMode 和 SpecSize 打包成一個 int 值來避免過多的內存分配,為了方便操作,其提供了打包和解包方法源碼如下:

//將 SpecMode 和 SpecSize 打包,獲取 MeasureSpec  
public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
//將 MeasureSpec 解包獲取 SpecMode
public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
//將 MeasureSpec 解包獲取 SpecSize
 public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

SpecMode 有三類,每一類都表示特殊的含義:

  1. UNSPECIFIED 父容器不對 View 有任何的限制,要多大給多大,這種情況下一般用于系統內部,表示一種測量的狀態。

  2. EXACTLY 父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值,它對應于LayoutParams 中的 match_parent 和具體的數值這兩種模式。

  3. AT_MOST 父容器指定了一個可用大小即 SpecSize,View 的大小不能大于這個值,具體是什么值要看不同 View 的具體實現。它對應于 LayoutParams 中的 wrap_content。

3. 理解MeasureSpec 和 LayoutParams 的對應關系

??對于DecorView,它的 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 來決定;對于普通 View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定,MeasureSpec一旦確定,onMeasure中就可以確定View的寬高。

??對普通的 View 來說,View的 measure過程是由其ViewGroup傳遞而來的,這里先看一下 ViewGroup 的 measureChildWithMargins 方法:

 * @param child 要被測量的 View
 * @param parentWidthMeasureSpec 父容器的 WidthMeasureSpec
 * @param widthUsed 父容器水平方向已經被占用的空間,比如被父容器的其他子 view 所占用的空間
 * @param parentHeightMeasureSpec 父容器的 HeightMeasureSpec
 * @param heightUsed 父容器豎直已經被占用的空間,比如被父容器的其他子 view 所占用的空間
 */
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {

   //第一步,獲取子 View 的 LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

   //第二步,獲取子 view 的 WidthMeasureSpec,其中傳入的幾個參數說明:
   //parentWidthMeasureSpec 父容器的 WidthMeasureSpec
   //mPaddingLeft + mPaddingRight view 本身的 Padding 值,即內邊距值
   //lp.leftMargin + lp.rightMargin view 本身的 Margin 值,即外邊距值
   //widthUsed 父容器已經被占用空間值
   // lp.width view 本身期望的寬度 with 值
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
     //獲取子 view 的 HeightMeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

// 第三步,根據獲取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec 
   //對子 view 進行測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

??上述方法會對子元素進行measure,在調用子元素的measure方法之前就會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec。從代碼上來看,子 view 的 MeasureSpec 的創建與父容器的 MeasureSpec 、子 view 本身的 LayoutParams 有關,此外還與 view 本身的 margin 和 padding 值有關,具體看一下ViewGroup的 getChildMeasureSpec 方法:

    /*
     * @param spec 父容器的 MeasureSpec,是對子 View 的約束條件
     * @param padding 當前 View 的 padding、margins 和父容器已經被占用空間值
     * @param childDimension View 期望大小值,即layout文件里設置的大小:可以是MATCH_PARENT,
     *WRAP_CONTENT或者具體大小, 代碼中分別對三種做不同的處理
     * @return 返回 View 的 MeasureSpec 值
     */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

   // 獲取父容器的 specMode,父容器的測量模式影響子 View  的測量模式
    int specMode = MeasureSpec.getMode(spec);
  // 獲取父容器的 specSize 尺寸,這個尺寸是父容器用來約束子 View 大小的
    int specSize = MeasureSpec.getSize(spec);
  // 父容器尺寸減掉已經被用掉的尺寸
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
     // 如果父容器是 EXACTLY 精準測量模式
    case MeasureSpec.EXACTLY:
        //如果子 View 期望尺寸為大于 0 的固定值,對應著 xml 文件中給定了 View 的具體尺寸大小
        //如 android:layout_width="100dp"
        if (childDimension >= 0) {
          //那么子 View 尺寸為期望值固定尺寸,測量模式為精準測量模式 EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
             //如果子 View 期望尺寸為 MATCH_PARENT 填充父布局
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 那么子 View 尺寸為 size 最大值,即父容器剩余空間尺寸,為精準測量模式 EXACTLY
          //即子 View 填的是 Match_parent, 那么父 View 就給子 View 自己的size(去掉padding),
          //即剩余全部未占用的尺寸, 然后告訴子 View 這是 Exactly 精準的大小,你就按照這個大小來設定自己的尺寸
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
          //如果子 View 期望尺寸為 WRAP_CONTENT ,包裹內容
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
          //子 View 尺寸為 size  最大值,即父容器剩余空間尺寸 ,測量模式為 AT_MOST 最大測量模式
          //即子 View 填的是 wrap_Content,那么父 View 就告訴子 View 自己的size(去掉padding),
          //即剩余全部未占用的尺寸,然后告訴子 View, 你最大的尺寸就這么多,不能超過這個值, 
          //具體大小,你自己根據自身情況決定最終大小。一般當我們繼承 View 基類進行自定義 View  的時候
          //需要在這種情況下計算給定 View 一個尺寸,否則當使用自定義的 View 的時候,使用 
          // android:layout_width="wrap_content" 屬性就會失效
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // 父容器為 AT_MOST 最大測量模式
    case MeasureSpec.AT_MOST:
           // 子 View 期望尺寸為一個大于 0的具體值,對應著 xml 文件中給定了 View 的具體尺寸大小
        //如 android:layout_width="100dp"
        if (childDimension >= 0) {
           //那么子 View 尺寸為期望固定值尺寸,為精準測量模式 EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
          //如果子 View 期望尺寸為 MATCH_PARENT 最大測量模式
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
             //子 View 尺寸為 size,測量模式為 AT_MOST  最大測量模式
          //即如果子 View 是 Match_parent,那么父 View 就會告訴子 View, 
          //你的尺寸最大為 size 這么大(父容器尺寸減掉已經被用掉的尺寸,即父容器剩余未占用尺寸),
          //你最多有父 View的 size 這么大,不能超過這個尺寸,至于具體多大,你自己根據自身情況決定。
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
             //同上
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // 父容器為 UNSPECIFIED 模式
    case MeasureSpec.UNSPECIFIED:
           // 子 View 期望尺寸為一個大于 0的具體值
        if (childDimension >= 0) {
             //那么子 View 尺寸為期望值固定尺寸,為精準測量模式 EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
           //如果子 View 期望尺寸為 MATCH_PARENT 最大測量模式
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
              //子 View 尺寸為 0,測量模式為 UNSPECIFIED
           // 父容器不對 View 有任何的限制,要多大給多大
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
           //如果子 View 期望尺寸為 WRAP_CONTENT ,包裹內容
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
             //子 View 尺寸為 0,測量模式為 UNSPECIFIED
             // 父容器不對 View 有任何的限制,要多大給多大
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

??上述方法的主要作用就是根據父容器的MesaureSpec,同時結合View本身的LayoutParams來確定子元素的Measure,參數中的padding是指父容器中的已經占用的控件大小,子元素的可用的控件的大小為父容器的尺寸減去padding。

int  specSize = MeasureSpec.getSize(spec);
int  size = Math.max(0,specSize- padding);

??getChildMeasureSpec() 清楚展示了普通View的MeasureSpec的創建規則。根據父容器的 MeasureSpec 和 view 本身的 LayoutParams 來確定子元素的 MeasureSpec 的整個過程,這個過程清楚的展示了普通 view 的 MeasureSpec 的創建規則,整理一下(圖片來源藝術探索)。



??只要提供了父容器的MeasureSpec和子元素的LayoutParams,就可以快速地確定子元素的MeasureSpec,有了MeasureSpec就可以進一步確定子元素測量后的大小。
總結:

  1. 當View是固定的寬高的時候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精確模式,并且大小是LayoutParams 中的大小。

  2. 當View的寬高是match_parent的時候,如果父容器的模式是精確模式,那么 View 也是精確模式,并且大小是父容器的剩余空間;如果父容器是最大模式,那么 View 也是最大模式,并且大小是不會超過父容器的剩余空間。

  3. 當 View 的寬高是 wrap_content 時,不管父容器的模式是精確模式還是最大模式,View 的模式總是最大模式,并且大小不超過父容器的剩余空間。

4. View 的measure過程

??View 的工作流程主要是指 measure、layout、draw 這三大流程,即測量、布局和繪制,其中 measure 確定 View 的測量寬和高,layout 確定 View 的最終寬和高及 View 的四個頂點位置,而 draw 是將 View 繪制到屏幕上。

4.1 measure過程

??measure過程要分情況,如果只是一個原始的View,那么通過measure方法就完成了測量過程,如果是ViewGroup,那么就需要首先測量自己的過程,然后再遍歷調用子元素的measure方法,各個子元素在地柜去執行這個流程,下面是對這兩種情況的分別討論。

4.1.1 View的Measure過程

??View 的 measure 過程由 measure 方法來完成, measure 方法是一個 final 類型,子類不可以重寫,而 View 的 measure() 方法中會調用 onMeasure 方法,因此我們只需要分析 onMeasure 方法即可,源碼如下:

 /**
  * @param widthMeasureSpec 父容器所施加的水平方向約束條件
  * @param heightMeasureSpec 父容器所施加的豎直方向約束條件
  */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  //設置 view 高寬的測量值
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

??上面方法很簡單,setMeasuredDimension方法就是給 View 設置了測量高寬的測量值,而這個測量值是通過 getDefaultSize 方法獲取,那么接著分析 getDefaultSize 方法:

 /**
   * @param size view 的默認尺寸,一般表示設置了android:minHeight屬性
   *  或者該View背景圖片的大小值 
   * @param measureSpec 父容器的約束條件 measureSpec
   * @return 返回 view 的測量尺寸
   */
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:
            //如果 測量模式為 UNSPECIFIED ,表示對父容器對子 view 沒有限制,那么 view 的測量尺寸為
          //默認尺寸 size
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        //如果測量模式為 AT_MOST 最大測量模式或者 EXACTLY 精準測量模式,
        //那么 View 的測量尺寸為 MeasureSpec 的 specSize
        //即父容器給定尺寸(父容器當前剩余全部空間大小)。
        result = specSize;
        break;
    }
    return result;
}

??getDefaultSize方法的邏輯很簡單,如果 測量模式為 UNSPECIFIED ,表示對父容器對子 view 沒有限制,那么 view 的測量尺寸為默認尺寸 size。如果測量模式為 AT_MOST 最大測量模式或者 EXACTLY 精準測量模式,那么 View 的測量尺寸為 MeasureSpec 的 specSize,即父容器給定尺寸(父容器當前剩余全部空間大小)。

??這里來分析一下 UNSPECIFIED 條件下 View 的測量高寬默認值 size 是通過 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 函數獲取,這兩個方法原理一樣,這里我們就看一下 getSuggestedMinimumHeight() 源碼:

protected int getSuggestedMinimumHeight() {
  return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

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

??從getSuggestedMinimumHeight代碼可以看出,如果 View 沒有背景,View 的高度就是 mMinHeight,這個 mMinHeight 是由 android:minHeight 這個屬性控制,可以為 默認為0,如果有背景,就返回 mMinHeight 和背景的最小高度兩者中的最大值;同理getSuggestedMinimumWidth也是一樣。

public  int  getMinimumWidth(){
    final  int  intrinsicWidth =  getInstrinsicWidth();
    return intrinsicWidth >0?intrinsicWidth  : 0;
}

??getMinimumWidth方法返回的就是Drawable的原始寬度,前提是這個Drawable的原始寬度,否則就返回0。

??從 getDefaultSize 方法可以看出,View 的高/寬是由 父容器傳遞進來的 specSize 決定,因此可以得出結論: 直接繼承自 View 的自定義控件需要重寫 onMeasure 方法并設置 wrap_content 時候的自身大小,而設置的具體值需要根據實際情況自己去計算或者直接給定一個默認固定值,否則在布局中使用 wrap_content 時候就相當于使用 match_parent ,因為在布局中使用 wrap_content 的時候,它的 specMode 是 AT_MOST 最大測量模式,在這種模式下 View 的寬/高等于 speceSize 大小,即父容器中可使用的大小,也就是父容器當前剩余全部空間大小,這種情況,很顯然,View 的寬/高就是等于父容器剩余空間的大小,填充父布局,這種效果和布局中使用 match_parent 一樣,解決這個問題代碼如下:

  @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  // 在 MeasureSpec.AT_MOST 模式下,給定一個默認值
  //其他情況下沿用系統測量規則即可
    if (widthSpecMode == MeasureSpec.AT_MOST
            && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWith, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWith, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    }
}

??上面代碼中在 widthSpecMode 或 heightSpecMode 為 MeasureSpec.AT_MOST 我們就給定一個對應的 mWith 和 mHeight 默認固定值寬高,而這個默認值沒有固定依據,需要我們根據自定義的 view 的具體情況去計算給定。

4.1.2 ViewGroup 的 measure 過程

??ViewGroup 除了完成自己的測量過程還會遍歷調用所有子 View 的measure方法,而且各個子 View 還會遞歸執行這個過程,我們知道 View Group 繼承自 View ,是一個抽象類,因此沒有重寫 View onMeasure 方法,也就是沒有提供具體如何測量自己的方法,但是它提供了一個 measureChildren 方法,定義了如何測量子 View 的規則,代碼如下:

/**
 * @param widthMeasureSpec 該 ViewGroup 水平方向約束條件
 * @param heightMeasureSpec 該 ViewGroup 豎直方向約束條件
 */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
      //逐一遍歷獲取得到 ViewGroup 中的子 View
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
          //對獲取到的 子 view 進行測量
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

我們再看一下對子 View 進行測量的 measureChild 方法 :

/**
 * @param child 要進行測量的子 view 
 * @param parentWidthMeasureSpec ViewGroup 對要進行測量的子 view 水平方向約束條件
 * @param parentHeightMeasureSpec  ViewGroup 對要進行測量的子 view 豎直方向約束條件
 */
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
   //第一步,獲取 View 的 LayoutParams
    final LayoutParams lp = child.getLayoutParams();
  //第二步,獲取 view 的 WidthMeasureSpec,其中傳入的幾個參數說明:
  //parentWidthMeasureSpec 父容器的 WidthMeasureSpec
  //mPaddingLeft + mPaddingRight view 本身的 Padding 值,即內邊距值
  // lp.width view 本身期望的寬度 with 值
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
  //同上
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
  // 第三步,根據獲取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec 
   //調用子 view 的 measure 方法,對子 view 進行測量,具體后面的測量邏輯就是和我們前面分析 
  // view 的測量過程一樣了。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

??measureChild的思想就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec來創建子元素的MeasureSpec,接著講MeasureSpec直接傳遞給View的measure方法來進行測量。

??ViewGroup 并沒有定義具體的測量過程,這是因為 ViewGroup 是一個抽象類,其不同子類具有不同的特性,導致他們的測量過程有所不同,不能有一個統一的 onMeasure 方法,所以其測量過程的 onMeasure 方法需要子類去具體實現,比如 LinearLayout 和 RelativeLayout 等,下面通過 LinearLayout 的 onMeasure 方法來分析一下 ViewGroup 的測量過程。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
      //垂直方向的 LinearLayout  測量方式
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
      //水平方向的 LinearLayout 測量方式
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

上面代碼可以看出 ViewGroup 內部測量方式分為垂直方向和水平方向,兩者原理基本一樣,下面看一下垂直方向的 LinearLayout 測量方式,由于這個方法代碼比較長,所以貼出重點部分:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
 ......................
    //記錄總高度
    float totalWeight = 0;
    final int count = getVirtualChildCount();
  //獲取測量模式
    final int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
 ...........
    //第1步,對 LinearLayout 中的子 view 進行第一次測量
    // See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }
        if (child.getVisibility() == View.GONE) {
            i += getChildrenSkipCount(child, i);
            continue;
        }
        if (hasDividerBeforeChildAt(i)) {
            mTotalLength += mDividerHeight;
        }
        //獲取子 view 的 LayoutParams 參數
        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
        totalWeight += lp.weight;
      //第1.1步,滿足該條件,第一次測量時不需要測量該子 view
        if (heightMode == View.MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
            // 滿足該條件的話,不需要現在計算該子視圖的高度。
            //因為 LinearLayout 的高度測量規格為 EXACTLY ,說明高度 LinearLayout 是固定的,
            //不依賴子視圖的高度計算自己的高度
            //lp.height == 0 && lp.weight > 0 說明子 view 使用了權重模式,即希望使用 LinearLayout 的剩余空間
            // 測量工作會在之后進行
            //相反,如果測量規格為 AT_MOST 或者 UNSPECIFIED ,LinearLayout
            // 只能根據子視圖的高度來確定自己的高度,就必須對所有的子視圖進行測量。
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            //標記未進行測量
            skippedMeasure = true;
        } else {
          //  else 語句內部是對子 view 進行第一次測量
            int oldHeight = Integer.MIN_VALUE;
            if (lp.height == 0 && lp.weight > 0) {
                // 如果 LiniearLayout 不是 EXACTLY 模式,高度沒給定,
              //說明 LiniearLayout 高度需要根據子視圖來測量,
                // 而此時子 view 模式為 lp.height == 0 && lp.weight > 0 ,是希望使用 LinearLayout 的剩余空間
                // 這種情況下,無法得出子 view 高度,而為了測量子視圖的高度,
              //設置子視圖 LayoutParams.height 為 wrap_content。
                oldHeight = 0;
                lp.height = LayoutParams.WRAP_CONTENT;
            }
            //該方法只是調用了 ViewGroup 的 measureChildWithMargins() 對子 view 進行測量
            // measureChildWithMargins() 方法在上面 4 MeasureSpec和LayoutParams的對應關系已經分析過
            measureChildBeforeLayout(
                    child, i, widthMeasureSpec, 0, heightMeasureSpec,
                    totalWeight == 0 ? mTotalLength : 0);
            if (oldHeight != Integer.MIN_VALUE) {
                lp.height = oldHeight;
            }
            // 獲取測量到的子 view 高度
            final int childHeight = child.getMeasuredHeight();
            final int totalLength = mTotalLength;
            //第2步, 重新計算 LinearLayout 的 mTotalLength 總高度
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                    lp.bottomMargin + getNextLocationOffset(child));
            if (useLargestChild) {
                largestChildHeight = Math.max(childHeight, largestChildHeight);
            }
        }
    ..........................
        //以下方法是對 LinearLayout 寬度相關的測量工作,不是我們關心的
        if (widthMode != View.MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
            .........................
    //以上方法是對 LinearLayout 寬度相關的測量工作
    if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
        mTotalLength += mDividerHeight;
    }
    //第3步,如果設置了 android:measureWithLargestChild="true"并且測量模式為 AT_MOST或者 UNSPECIFIED
    // 重新計算 mTotalLength 總高度
    if (useLargestChild &&
            (heightMode == View.MeasureSpec.AT_MOST || heightMode == View.MeasureSpec.UNSPECIFIED)) {
        mTotalLength = 0;
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
            if (child.getVisibility() == GONE) {
                i += getChildrenSkipCount(child, i);
                continue;
            }
            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                    child.getLayoutParams();
            // Account for negative margins
            final int totalLength = mTotalLength;
            //每個子視圖的高度為:最大子視圖高度 + 該子視圖的上下外邊距
            mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                    lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
        }
    }
    // Add in our padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    //第4步,根據 heightMeasureSpec 測量模式 和已經測量得到的總高度 heightSize
    //來確定得到最終 LinearLayout 高度和狀態
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
          
    //分割線=================以上代碼就完成了對  LinearLayout 高度和狀態 的測量
    //第5步,下面代碼是根據已經測量得到的 LinearLayout 高度來重新測量確定各個子 view 的大小
    //獲取 LinearLayout 高度值
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    //獲取最終測量高度和經過測量各個子 view 得到的總高度差值
    int delta = heightSize - mTotalLength;
    //第5.1步(第5步中第1小步),如果在上面第一次測量子 view 的過程中有未進行測量的 view 那么執行下面代碼
    if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
        float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
        mTotalLength = 0;
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            float childExtra = lp.weight;
            if (childExtra > 0) {
                // 計算 weight 屬性分配的大小,可能為負值
                int share = (int) (childExtra * delta / weightSum);
                weightSum -= childExtra;
                delta -= share;
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        mPaddingLeft + mPaddingRight +
                                lp.leftMargin + lp.rightMargin, lp.width);
                // TODO: Use a field like lp.isMeasured to figure out if this
                // child has been previously measured
                if ((lp.height != 0) || (heightMode != View.MeasureSpec.EXACTLY)) {
                    // 子視圖在第一次測量時候已經測量過
                    // 基于上次測量值再次進行新的測量
                    int childHeight = child.getMeasuredHeight() + share;
                    if (childHeight < 0) {
                        childHeight = 0;
                    }
                    // 調用子 view 的 measure 方法進行測量,后面邏輯就是 view 的測量邏輯
                    child.measure(childWidthMeasureSpec,
                            View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY));
                } else {
                    // 子視圖第一次測量,即第一步進行測量的時候未得到測量
                    //對 view 進行測量
                    child.measure(childWidthMeasureSpec,
                            View.MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                    View.MeasureSpec.EXACTLY));
                }
                // Child may now not fit in vertical dimension.
                childState = combineMeasuredStates(childState, child.getMeasuredState()
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
            }
            // 處理子視圖寬度
            final int margin =  lp.leftMargin + lp.rightMargin;
           ...........................
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        // TODO: Should we recompute the heightSpec based on the new total length?
    } else {
        //第5.2步(第5步中第2小步)執行到這里的代碼,表明 view 是已經測量過的
        alternativeMaxWidth = Math.max(alternativeMaxWidth,
                weightedMaxWidth);
        // We have no limit, so make all weighted views as tall as the largest child.
        // Children will have already been measured once.
        if (useLargestChild && heightMode != View.MeasureSpec.EXACTLY) {
            for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                float childExtra = lp.weight;
                //如果 view 使用了權重即 childExtra > 0,使用最大子視圖高度進行重新測量
                //否則不進行測量,保持第一次測量值,那么由于 LinearLayout 的高度使用了子 view 最大高度 ,
                // 但是子視圖沒有進行重新測量,沒有進行拉伸,可能造成空間剩余。
                if (childExtra > 0) {
                    //使用最大子視圖高度進行重新測量子 view 
                    child.measure(
                            View.MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                    View.MeasureSpec.EXACTLY),
                            View.MeasureSpec.makeMeasureSpec(largestChildHeight,
                                    View.MeasureSpec.EXACTLY));
                }
            }
        }
    }
    if (!allFillParent && widthMode != View.MeasureSpec.EXACTLY) {
        maxWidth = alternativeMaxWidth;
    }
    maxWidth += mPaddingLeft + mPaddingRight;
    // Check against our minimum width
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    //第6步,最終設置 LinearLayout 的測量高寬
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
    if (matchWidth) {
        forceUniformWidth(count, heightMeasureSpec);
    }
}

??從上面的這段代碼可以看出,系統會遍歷子元素并對每個子元measureChildBeforeLayout 方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就可以一次進入measure過程,并且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每次測量一個子元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度和子元素在豎直方向上的margin等。

??以上代碼就是對 LinearLayout onMeasure 分析過程,整個過程原理已經在代碼中加以注釋說明,這里我們重點分析一下 resolveSizeAndState(heightSize, heightMeasureSpec, 0) 這個方法是如何實現最終確定 LinearLayout 高度值的,方法如下:

/**
 * @param size view 想要的大小,也就是根據子 view 高度測量得到的高度值.
 * @param measureSpec 父容器的約束條件
 * @param childMeasuredState 子 view 的測量信息
 * @return Size 返回得到的測量值和狀態
 */
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) {
        //如果是 AT_MOST 最大測量模式 ,那么總高度值為測量得到的 size 值,但是最大不能超過 specSize 規定值
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
              //如果測量得到的 size 值超過 specSize 值,LinearLayout 高度就為 specSize 值
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
              //如果測量得到的 size 值未超過 specSize 值,LinearLayout 高度就為 size 值
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
       //如果是 EXACTLY 精準測量模式,即 LinearLayout 值為固定值,那么 最終 LinearLayout 高度值就為 specSize 值
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        // 如果是 UNSPECIFIED 測量模式,即對子 view 沒有限制 , LinearLayout 高度值就為 size
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

??以上代碼總結起來就是 LinearLayout 會根據測量子 View 的情況和 MeasureSpec 約束條件來決定自己最終的大小,具體來說就是如果它的布局中高度才用 具體數值,那么它的測量過程和 View 一致,即高度為 specSize 值,如果它的布局中使用 wrap_content 那么它的高度是所有子 View 高度總和,但是不能超過父容器剩余空間。

最后對整個測量過程總結一下就是分為以下幾步:

  1. 對 LinearLayout 中的子 View 進行第一次遍歷測量,主要是通過 measureChildBeforeLayout 這個方法,這個方法內部會調用 measureChildWithMargins 方法,而在 measureChildWithMargins 方法內部會去調用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法進行測量。在這次的測量過程中,如果滿足了第1.1步測量條件的子 view 不需要進行測量,會在后面的第5.1步中進行測量。
  2. 根據測量各個子 View 的高度會得到一個初步的 LinearLayout 總高度 mTotalLength 值。
  3. 如果 LinearLayout 設置了 android:measureWithLargestChild=”true” 屬性并且測量模式為 AT_MOST或者 UNSPECIFIED 重新計算 mTotalLength 總高度。
  4. 根據 LinearLayout 的 heightMeasureSpec 測量模式 和已經測量得到的總高度 mTotalLength ,來確定得到最終 LinearLayout 高度和狀態 。
  5. 根據已經測量得到的 LinearLayout 高度來重新測量確定各個子 View 的大小。
  6. 最終執行 setMeasuredDimension 方法設置 LinearLayout 的測量高寬。

5. 在實際measure中View無法獲取寬高信息的問題解決

??View的measure過程和Activity的生命周期實際上是不同步的。所以將當View還沒有測量完畢,那么獲取的寬高就是0,這里有四種方法解決這個問題:

1. Activity/View#onWindowFocusChanged

??onWindowFocusChanged方法表示View已經初始化了,寬高已經準備好了,這時候獲取就沒有問題。需要注意的是,onWindowFocusChanged會被調用多次,當Activity的窗口得到和失去焦點時均會被調用一次。具體說,當Activity繼續執行和暫停執行的時候,onWindowFocusChanged均會被調用,如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地調用。代碼如下:

public void onWindowFocusChanged(boolean hasWindowFocus) {
         super.onWindowFocusChanged(hasWindowFocus);
       if(hasWindowFocus){
       int width=view.getMeasuredWidth();
       int height=view.getMeasuredHeight();
      }      
  }
2. view.post(runnable)

??通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View也初始化好了,代碼如下:

@Override
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width=view.getMeasuredWidth();
            int height=view.getMeasuredHeight();
        }
    });
}
3. ViewTreeObsever

??使用ViewTreeObsever的眾多回調可以完成這個功能,比如使用onGlobalLayoutListener 這個接口,當View樹狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout 方法將被回調。伴隨著View樹的變化,這個方法也會被多次調用。

@Override
protected void onStart() {
    super.onStart();
    ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width=view.getMeasuredWidth();
            int height=view.getMeasuredHeight();
        }
    });
}
4. view.measure(int widthMeasureSpec, int heightMeasureSpec)

通過手動對View進行measure來得到View的寬高。這種方式比較復雜,需要分情況處理。根據View的LayoutParams來分:
(1) match_parent :無法measure出具體的寬高。因為在View的measure過程中,構造這種MeasureSpec需要知道parentSize,即知道父容器的剩余空間,match_parent是一個不確定值,所以理論上不能測量大小。

(2) wrap_content : 可以采用設置最大值方法進行 measure :

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

??通過分析 MeasureSpec 的實現可以得知 View 的尺寸是使用 30 位的二進制表示,也就是說最大是 30 個 1 即(2^30-1),也就是 (1 << 30) - 1 ),在最大化模式下,使用 View 能支持的最大值去構造 MeasureSpec 是合理的。為什么這樣就合理呢?我們前面分析在子 View 使用 wrap_content 模式的時候,其測量規則是根據自身的情況去測量尺寸,但是不能超過父容器的剩余空間的最大值,換句話說就是父容器給子 View 一個最大值,然后告訴子 View 你自己看著辦,但是別超過這個尺寸就行,但是現在我們自己去測量的時候不知道父容器給定的 MeasureSpec 情況, 也就是不知道父容器給多大的限定值,需要自己去構造一個MeasureSpec ,那么這個最大值我們給定多少合適呢?所以這里干脆就給一個 View 所能支持的最大值,然子 View 根據自身情況去測量,怎么也不能超過這個值就行了。

??關于View的measure,有兩個常見的錯誤用法。理由是首先其違背了系統的內部實現規范(因為不能通過錯誤的MeasureSpec來得到合法的SpecMode,從而導致measure過程出錯),其次是不能保證一定能measure出正確的結果。

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int  heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1,MeasureSpec.UNSPECIFIED);
view.measure (widthMeasureSpec,heightMeasureSpec);
view.measure(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);

(3)具體的數值(dp/px): 例如100px

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

6. View的Layout過程

??Layout過程是ViewGroup的用來確定子元素的位置的,當ViewGroup的位置確定了之后,它在onLayout中會遍歷所有的子元素,并調用其layout方法,在layout方法中onLayout方法又會被調用。Layout過程比較簡單,layout方法確定了View本身的位置,onlayout方法會確定所有子元素的位置,代碼如下:

 /* 
 *@param l view 左邊緣相對于父布局左邊緣距離
 *@param t view 上邊緣相對于父布局上邊緣位置
 *@param r view 右邊緣相對于父布局左邊緣距離
 *@param b view 下邊緣相對于父布局上邊緣距離
 */
 public void layout(int l, int t, int r, int b) {
 if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
      onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
      mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
  }
  //記錄 view 原始位置
  int oldL = mLeft;
  int oldT = mTop;
  int oldB = mBottom;
  int oldR = mRight;
//第1步,調用 setFrame 方法 設置新的 mLeft、mTop、mBottom、mRight 值,
//設置 View 本身四個頂點位置
//并返回 changed 用于判斷 view 布局是否改變
  boolean changed = isLayoutModeOptical(mParent) ?
          setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
             //第二步,如果 view 位置改變那么調用 onLayout 方法設置子 view 位置
             if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    //調用 onLayout
      onLayout(changed, l, t, r, b);
      mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
      ListenerInfo li = mListenerInfo;
      if (li != null && li.mOnLayoutChangeListeners != null) {
          ArrayList<OnLayoutChangeListener> listenersCopy =
                  (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
          int numListeners = listenersCopy.size();
          for (int i = 0; i < numListeners; ++i) {
              listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
          }
      }
  }

??layout 方法大致流程:先通過上面代碼第一步調用 setFrame 設置 view 本身四個頂點位置,其中 setOpticalFrame 內部也是調用 setFrame 方法來完成設置的,即為 View 的4個成員變量(mLeft,mTop,mRight,mBottom)賦值,View 的四個頂點一旦確定,那么 View 在父容器中的位置就確定了,接著進行第二步,調用 onLayout 方法,這個方法用途是父容器確定子 View 位置,和 onMeasure 方法類似, onLayout 方法的具體實現同樣和具體布局有關,所以 View 和 ViewGroup 中都沒有真正實現 onLayout 方法,都是一個空方法。 View 的 onLayout 方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
   if (mOrientation == VERTICAL) {
       layoutVertical(l, t, r, b);
   } else {
       layoutHorizontal(l, t, r, b);
   }
}

?和 onMeasure 類似,這里也是分為豎直方向和水平方向的布局安排,二者原理一樣,我們選擇豎直方向的 layoutVertical 來進行分析,這里給出主要代碼如下:

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;
  //記錄子 View 上邊緣相對于父容器上邊緣距離
    int childTop;
    //記錄子 View 左邊緣相對于父容器左邊緣距離
    int childLeft;
  //第1步,主要是根據不同的 gravity 屬性來確定子元素的 child 的位置
    switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;
           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
   ...............................
//第2步,循環遍歷子 view 
    for (int i = 0; i < count; i++) {
      //獲取指定位置 view 
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
          //第2.1步,如果 view 可見,獲取 view  的測量寬/高
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            //獲取 view  的 LayoutParams 參數
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
        .............
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            
            childTop += lp.topMargin;
          //第3步,設置子 view 位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
          //第4步,重新計算子 view 的 頂部 top 位置,也就是每增加一個子 view 
          //下一個子 view 的 top 頂部位置就會相應的增加
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            i += getChildrenSkipCount(child, i);
        }
    }
}

??簡單梳理下整個流程,此方法會遍歷所有子 view ,并調用 setChildFrame 方法來設定子元素位置,然后重新計算 childTop ,childTop 隨著子元素的遍歷而逐漸增大,這就意味著后面的子元素會被放置在當前子元素的下方,這正是我們平時使用豎直方向 LinearLayout 的特性。這里我們看一下第三步執行的 setChildFrame 方法類設置子元素位置方法代碼:

private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}

??我們注意到,setChildFrame中的width和height 實際上就是子元素的測量寬高,從下面的代碼可以看出:

final int childWidth  = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child,childLeft,childTop+getLocationOffset(child),childWidth,childHeight);

??在layout方法中會設置setFrame去設置子元素的四個頂點的位置,

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

??也就是說在 LinearLayout 中其子視圖顯示的寬和高由 measure 過程來決定的,因此 measure 過程的意義就是為 layout 過程提供視圖顯示范圍的參考值。為什么說是提供參考值呢?因為 layout 過程中的4個參數 left, top, iwidth, height 完全可以由視圖設計者任意指定,而最終視圖的布局位置和大小完全由這4個參數決定,measure 過程得到的mMeasuredWidth 和 mMeasuredHeight 提供了視圖大小的測量值,只是提供一個參考一般情況下我們使用這個參考值,但我們完全可以不使用這兩個值,而自己在 layout 過程中去設定一個值,可見 measure 過程并不是必須的。

??說到這里就不得說一下 getWidth() 、getHeight() 和 getMeasuredWidth()、getMeasuredHeight() 這兩對函數之間的區別,即 View 的測量寬/高和最終顯示寬/高之間的區別。首先我們看一下 getWith() 和 getHeight() 方法的具體實現:

public final int getWidth() {
    return mRight - mLeft;
}
 public final int getHeight() {
        return mBottom - mTop;
    }

??通過 getWith() 和 getHeight() 源碼和上面 setChildFrame(View child, int left, int top, int width, int height) 方法設置子元素四個頂點位置的四個變量 mLeft、mTop、mRight、mBottom 的賦值過程來看,默認情況下 getWidth() 、getHeight() 方法返回的值正好就是 view 的測量寬/高,只不過 view 的測量寬/高形成于 view 的measure 過程,而最終寬/高形成于 view 的 layout 方法中,但是對于特殊情況,兩者的值是不相等的,就是我們在 layout 過程中不按默認常規套路出牌,即不使用 measure 過程得到的 mMeasuredWidth 和 mMeasuredHeight ,而是人為的去自己根據需要設定的一個值的情況,例如以下代碼,重寫 view 的 layout 方法:

public void layout(int l, int t, int r, int b) {
  //在得到的測量值基礎上加100
    super.layout(int l, int t, int r+100, int b+100);
    }

上面代碼會導致在任何情況下 view 的最終寬/高總會比測量寬高大100px。

7. View的draw過程

??Draw過程比較簡單,它的作用就是講View繪制到屏幕上。View的繪制過程有以下幾步:

  1. 繪制背景 background.draw(canvas)
  2. 繪制自己 (onDraw)
  3. 繪制children(dispatchDraw)
  4. 繪制裝飾 (onDrawScrollBars)

源碼如下:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    /*
     * 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
  //繪制背景
    int saveCount;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
      //調用 onDraw 方法,繪制自己本身內容,這個方法是個空方法,沒有具體實現,
      //因為每個視圖的內容部分肯定都是各不相同的,這部分的功能交給子類來去實現,
      //如果要自定義 view ,需要重載該方法完成繪制工作
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
      //繪制子視圖
      //View 中的 dispatchDraw()方法也是一個空方法,因為 view 本身沒有子視圖,所以不需要,
      //而 ViewGroup 的 dispatchDraw() 方法中就會有具體的繪制代碼,來實現子視圖的繪制工作
        dispatchDraw(canvas);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        // Step 6, draw decorations (foreground, scrollbars)
      //繪制裝飾
      //對視圖的滾動條進行繪制,其實任何一個視圖都是有滾動條的,只是一般情況下都沒有讓它顯示出來,
      //而例如像 ListView 等控件是進行了顯示而已。
        onDrawForeground(canvas);
        // we're done...
        return;
    }

??View的繪制過程的傳遞是dispatchDraw來實現的,dispatchDraw會遍歷調用所有的子元素的draw方法,如此draw事件就一層層地傳遞下去。View有一個特殊的方法setWillNotDraw,先看看它的源碼:

/**
 * If this view doesn't do any drawing on its own, set this flag to
 * allow further optimizations. By default, this flag is not set on
 * View, but could be set on some View subclasses such as ViewGroup.
 *
 * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
 * you should clear this flag.
 *
 * @param willNotDraw whether or not this View draw on its own
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

??看注釋部分大概意思是,如果一個 View 不需要繪制任何內容,那么設置這個標記位為 true 后,系統會進行相應的優化,默認情況下 View 沒有啟動這個默認標記位,但 viewGroup 默認啟用這個標記位,這個標記位對實際開發的意義是:當我們的自定義的控件繼承自 viewGroup 并且本身不具備繪制功能的時候,就可以開啟這個標記位,從而便于系統進行后續的優化工作,當我們明確知道 viewGrop 需要通過 onDraw 來繪制本身內容時,需要我們去關閉 WILL_NOT_DRAW 這個標記位。

參考鏈接:
http://yongyu.itscoder.com/2016/09/11/view_measure/
http://yongyu.itscoder.com/2016/10/05/view_layout_and_draw/

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容