Android View的繪製流程

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 是二進制存儲的。


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

上面的代碼對於出來的佈局是下面的一張圖

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

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

 

這張圖在下面分析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 圖:

 

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

Paste_Image.png

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

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.

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.

最終算出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的遞歸流程。

 

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


原文鏈接:https://www.jianshu.com/p/5a71014e7b1b
 

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