Android View的繪制流程

re7568 8年前發布 | 23K 次閱讀 Android Android開發 移動開發

View的繪制和事件處理是兩個重要的主題,上一篇《圖解 Android事件分發機制》已經把事件的分發機制講得比較詳細了,這一篇是針對View的繪制,View的繪制如果你有所了解,基本分為measure、layout、draw 過程,其中比較難理解就是measure過程,所以本篇文章大幅筆地分析measure過程,相對講得比較詳細,文章也比較長,如果你對View的繪制還不是很懂,對measure過程掌握得不是很深刻,那么耐心點,看完這篇文章,相信你會有所收獲的。

Measure過程

對于測量我們來說幾個知識點,了解這幾個知識點,之后的實例分析你才看得懂。
1、MeasureSpec 的理解
對于View的測量,肯定會和MeasureSpec接觸,MeasureSpec是兩個單詞組成,翻譯過來“測量規格”或者“測量參數”,很多博客包括官方文檔對他的說明基本都是“一個MeasureSpec封裝了從父容器傳遞給子容器的布局要求”,這個MeasureSpec 封裝的是父容器傳遞給子容器的布局要求,而不是父容器對子容器的布局要求,“傳遞” 兩個字很重要,更精確的說法應該這個MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過簡單的計算得出一個針對子View的測量要求,這個測量要求就是MeasureSpec。

  • 大家都知道一個MeasureSpec是一個大小跟模式的組合值,MeasureSpec中的值是一個整型(32位)將size和mode打包成一個Int型,其中高兩位是mode,后面30位存的是size,是為了減少對象的分配開支。MeasureSpec 類似于下圖,只不過這邊用的是十進制的數,而MeasureSpec 是二進制存儲的。

    Android View的繪制流程


    注:-1 代表的是EXACTLY,-2 是AT_MOST
  • MeasureSpec一共有三種模式

    UPSPECIFIED : 父容器對于子容器沒有任何限制,子容器想要多大就多大
    EXACTLY: 父容器已經為子容器設置了尺寸,子容器應當服從這些邊界,不論子容器想要多大的空間。
    AT_MOST:子容器可以是聲明大小內的任意大小

很多文章都會把這三個模式說成這樣,當然其實包括官方文檔也是這樣表達的,但是這樣并不好理解。特別是如果把這三種模式又和MATCH_PARENT和WRAP_CONTENT 聯系到一起,很多人就懵逼了。如果從代碼上來看view.measure(int widthMeasureSpec, int heightMeasureSpec) 的兩個MeasureSpec是父類傳遞過來的,但并不是完全是父View的要求,而是父View的MeasureSpec和子View自己的LayoutParams共同決定的,而子View的LayoutParams其實就是我們在xml寫的時候設置的layout_width和layout_height 轉化而來的。我們先來看代碼會清晰一些:

父View的measure的過程會先測量子View,等子View測量結果出來后,再來測量自己,上面的measureChildWithMargins就是用來測量某個子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 就是充滿父容器,wrap_content 就是自己多大就多大, 我們寫代碼的時候特別爽,我們編碼方便的時候,google就要幫我們計算你match_parent的時候是多大,wrap_content的是多大,這個計算過程,就是計算出來的父View的MeasureSpec不斷往子View傳遞,結合子View的LayoutParams 一起再算出子View的MeasureSpec,然后繼續傳給子View,不斷計算每個View的MeasureSpec,子View有了MeasureSpec才能更測量自己和自己的子View。

2、上述代碼如果這么來理解就簡單了

  • 如果父View的MeasureSpec 是EXACTLY,說明父View的大小是確切的,(確切的意思很好理解,如果一個View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大)。

    1、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是確切,子View的大小又MATCH_PARENT(充滿整個父View),那么子View的大小肯定是確切的,而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY

    2、如果子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再實現自己的測量。

    3、如果如果子View 的layout_xxxx是確定的值(200dp),那么就更簡單了,不管你父View的mode和size是什么,我都寫死了就是200dp,那么控件最后展示就是就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是這么大,所以這種情況MeasureSpec 的mode = EXACTLY 大小size=你在layout_xxxx 填的那個值。

  • 如果父View的MeasureSpec 是AT_MOST,說明父View的大小是不確定,最大的大小是MeasureSpec 的size值,不能超過這個值。

    1、如果子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的大小。

    2、如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不確定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。

    3、如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的。

  • 如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示沒有任何束縛和約束,不像AT_MOST表示最大只能多大,不也像EXACTLY表示父View確定的大小,子View可以得到任意想要的大小,不受約束

    1、如果子View 的layout_xxxx是MATCH_PARENT,因為父View的MeasureSpec是UNSPECIFIED,父View自己的大小并沒有任何約束和要求,
    那么對于子View來說無論是WRAP_CONTENT還是MATCH_PARENT,子View也是沒有任何束縛的,想多大就多大,沒有不能超過多少的要求,一旦沒有任何要求和約束,size的值就沒有任何意義了,所以一般都直接設置成0

    2、同上...

    3、如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的(記住,只有設置的確切的值,那么無論怎么測量,大小都是不變的,都是你寫的那個值)

到此為止,你是否對MeasureSpec 和三種模式、還有WRAP_CONTENT和MATCH_PARENT有一定的了解了,如果還有任何問題,歡迎在我簡書(用戶名:Kelin)評論里留言。

2、View的測量過程主要是在onMeasure()方法
打開View的源碼,找到measure方法,這個方法代碼不少,但是測量工作都是在onMeasure()做的,measure方法是final的所以這個方法也不可重寫,如果想自定義View的測量,你應該去重寫onMeasure()方法

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

3、View的onMeasure 的默認實現
打開View.java 的源碼來看下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來說它的默認高寬是怎么獲取的。

//獲取的是android:minHeight屬性的值或者View背景圖片的大小值
protected int getSuggestedMinimumWidth() { 
   return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); 
} 
//@param size參數一般表示設置了android:minHeight屬性或者該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:        //表示該View的大小父視圖未定,設置為默認值 
     result = size;  
     break;    
   case MeasureSpec.AT_MOST:    
   case MeasureSpec.EXACTLY:        
     result = specSize;  
     break;   
 }    
return result;
}

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的大小。

4、ViewGroup的Measure過程
ViewGroup 類并沒有實現onMeasure,我們知道測量過程其實都是在onMeasure方法里面做的,我們來看下FrameLayout 的onMeasure 方法,具體分析看注釋哦。

//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,根據傳下來的這個MeasureSpec測量自己就好了。
     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);  
     ....
     ....
   }
}
.....
.....
//所有的孩子測量之后,經過一系類的計算之后通過setMeasuredDimension設置自己的寬高,
//對于FrameLayout 可能用最大的字View的大小,對于LinearLayout,可能是高度的累加,
//具體測量的原理去看看源碼。總的來說,父View是等所有的子View測量結束之后,再來測量自己。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),        
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
....
}

到目前為止,基本把Measure 主要原理都過了一遍,接下來我們會結合實例來講解整個match的過程,首先看下面的代碼:

<?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>

上面的代碼對于出來的布局是下面的一張圖

Android View的繪制流程

對于上面圖可能有些不懂,這邊做下說明:

整個圖是一個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的布局圖如下:

Android View的繪制流程


這張圖在下面分析measure,會經常用到,主要用于了解遞歸的時候view 的measure順序

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

既然我們知道整個View的Root是DecorView,那么View的繪制是從哪里開始的呢,我們知道每個Activity 均會創建一個 PhoneWindow對象,是Activity和整個View系統交互的接口,每個Window都對應著一個View和一個ViewRootImpl,Window和View通過ViewRootImpl來建立聯系,對于Activity來說,ViewRootImpl是連接WindowManager和DecorView的紐帶,繪制的入口是由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 圖:

Android View的繪制流程

注:
1、-1 代表的是EXACTLY,-2 是AT_MOST
2、由于屏幕的像素是1440x2560,所以DecorView 的MeasureSpec的size 對應于這兩個值

那么接下來在FrameLayout 的onMeasure()方法DecorView開始for循環測量自己的子View,測量完所有的子View再來測量自己,由下圖可知,接下來要測量ViewRoot的大小

Android View的繪制流程

//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圖如下:

Android View的繪制流程


算出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的方法,

Android View的繪制流程

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):

Android View的繪制流程

Paste_Image.png

Content(android.R.id.content) 是FrameLayout,遞歸調用開始準備計算id/linear的MeasureSpec,我們先給出結果:

Android View的繪制流程

圖中有兩個要注意的地方:
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.

Android View的繪制流程

算出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 方法執行調試過程中,我們發現下面的代碼:

Android View的繪制流程

Paste_Image.png

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.

Android View的繪制流程

Paste_Image.png

id/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.

Android View的繪制流程

最終算出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 分析到此為止,如有不懂,評論留言(簡書:kelin)

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是用于處理ViewGroup增加和刪除子視圖的動畫效果,也就是說如果當前ViewGroup未添加LayoutTransition動畫,或者LayoutTransition動畫此刻并未運行,那么調用super.layout(l, t, r, b),繼而調用到ViewGroup中的onLayout,否則將mLayoutSuppressed設置為true,等待動畫完成時再調用requestLayout()。
這個函數是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只是一個空實現,一般情況下我們也不需要重載該函數,:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  

    }

對于ViewGroup 來說,唯一的差別就是ViewGroup中多了關鍵字abstract的修飾,要求其子類必須重載onLayout函數。

@Override  
protected abstract void onLayout(boolean changed,  
        int l, int t, int r, int b);

而重載onLayout的目的就是安排其children在父視圖的具體位置,那么如何安排子View的具體位置呢?

 int 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 過程相對簡單些,分析就到此為止。

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

2、第三步,對View的內容進行繪制。
onDraw(canvas) 方法是view用來draw 自己的,具體如何繪制,顏色線條什么樣式就需要子View自己去實現,View.java 的onDraw(canvas) 是空實現,ViewGroup 也沒有實現,每個View的內容是各不相同的,所以需要由子類去實現具體邏輯。

3、第4步 對當前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()函數進行具體的繪制了。

4、第6步 對View的滾動條進行繪制
不是重點,知道有這東西就行,onDrawScrollBars 的一句注釋 :Request the drawing of the horizontal and the vertical scrollbar. The scrollbars are painted only if they have been awakened first.

一張圖看下整個draw的遞歸流程。

Android View的繪制流程

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

 


閱讀原文

 本文由用戶 re7568 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!