轉自:Android View的繪製流程 寫得太好了,本來還想自己寫的,奈何肚裡墨水有限,直接轉吧。正所謂前人種樹,後人乘涼。。 View的繪製和事件處理是兩個重要的主題,上一篇《圖解 Android事件分發機制》已經把事件的分發機制講得比較詳細了,這一篇是針對View的繪製,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的值就沒有任何意義了,所以一般都直接設置成02、同上...
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順序
上面的代碼對於出來的佈局是下麵的一張圖
對於上面圖可能有些不懂,這邊做下說明:
整個圖是一個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):
Content(android.R.id.content) 是FrameLayout,遞歸調用開始準備計算id/linear的MeasureSpec,我們先給出結果:
圖中有兩個要註意的地方:
1、id/linear的heightMeasureSpec 的mode=AT_MOST,因為id/linear 的LayoutParams 的layout_height="wrap_content"
2、id/linear的heightMeasureSpec 的size 少了200px, 由上面的代碼
padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
int size = Math.max(0, specSize - padding);
由於id/linear 的 android:layout_marginTop="50dp" 使得lp.topMargin=200px (本設備的density=4,px=4*pd),在計算後id/linear的heightMeasureSpec 的size 少了200px。(佈局代碼前面已給出,可自行查看id/linear 控制項xml中設置的屬性)
linear.measure接著往下算linear的子View的的MeasureSpec,看下View 層級圖,往下走應該是id/text,接下來是計算id/text的MeasureSpec,直接看圖,mode=AT_MOST ,size 少了280,別問我為什麼 ...specSize - padding.
算出id/text 的MeasureSpec 後,接下來text.measure(childWidthMeasureSpec, childHeightMeasureSpec);準備測量id/text 的高寬,這時候已經到底了,id/text是TextView,已經沒有子類了,這時候跳到TextView的onMeasure方法了。TextView 拿著剛纔計算出來的heightMeasureSpec(mode=AT_MOST,size=1980),這個就是對TextView的高度和寬度的約束,進到TextView 的onMeasure(widthMeasureSpec,heightMeasureSpec) 方法,在onMeasure 方法執行調試過程中,我們發現下麵的代碼:
TextView字元的高度(也就是TextView的content高度[wrap_content])測出來=107px,107px 並沒有超過1980px(允許的最大高度),所以實際測量出來TextView的高度是107px。
最終算出id/text 的mMeasureWidth=1440px,mMeasureHeight=107px。
貼一下佈局代碼,免得你忘了具體佈局。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="50dp" android:background="@android:color/holo_blue_dark" android:paddingBottom="70dp" android:orientation="vertical"> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/material_blue_grey_800" android:text="TextView" android:textColor="@android:color/white" android:textSize="20sp" /> <View android:id="@+id/view" android:layout_width="match_parent" android:layout_height="150dp" android:background="@android:color/holo_green_dark" /> </LinearLayout>
TextView的高度已經測量出來了,接下來測量id/linear的第二個child(id/view),同樣的原理測出id/view的MeasureSpec.
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的遞歸流程。
到此整個繪製過程基本講述完畢了。
註:【轉載請註明,問題可提問,喜歡可打賞,博客持續更新,歡迎關註 簡書:kelin】
文/Kelin(簡書作者)
原文鏈接:http://www.jianshu.com/p/5a71014e7b1b
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。