文章目錄
View的工作流程
View的工作流程,指的就是measure、layout和draw。其中,measure用來測量View的寬和高,layout用來確定View的位置,draw則用來繪製View。
View的工作流程入口
DecorView被加載到Window中
當DecorView創建完畢,要加載到Window中時,我們需要先了解一下Activity的創建過程。當我們調用Activity的startActivity方法時,最終是調用ActivityThread的handleLaunchActivity方法來創建Activity的
調用 performLaunchActivity 方法來創建 Activity,在這裏面會調用到Activity的onCreate方法,從而完成DecorView的創建。
handleResumeActivity方法
註釋1處的performResumeActivity方法中會調用Activity的onResume方法。接着往下看,註釋2處得到了DecorView。註釋3處得到了WindowManager,WindowManager是一個接口並且繼承了接口ViewManager。在註釋4處調用WindowManager的addView方法,WindowManager 的實現類是WindowManagerImpl,所以實際調用的是 WindowManagerImpl 的addView方法。具體代碼如下所示:
在 WindowManagerImpl 的 addView 方法中,又調用了 WindowManagerGlobal 的 addView方法,代碼如下所示:
註釋1處創建了ViewRootImpl實例,在註釋2處調用了ViewRootImpl的setView方法並將DecorView作爲參數傳進去,這樣就把DecorView加載到了Window中。當然界面仍不會顯示出什麼來,因爲View的工作流程還沒有執行完,還需要經過measure、layout以及draw纔會把View繪製出來。
將 DecorView 加載到 Window 中,是通過 ViewRootImpl 的 setView 方法。ViewRootImpl還有一個方法PerformTraveals,這個方法使得ViewTree開始View的工作流程,代碼如下所示:
主要執行了3個方法,分別是performMeasure、performLayout和performDraw,在其方法的內部又會分別調用View的measure、layout和draw方法。需要注意的是,performMeasure方法中需要傳入兩個參數,分別是 childWidthMeasureSpec 和 childHeightMeasureSpec。要了解這兩個參數,需要了解MeasureSpec。
MeasureSpec
MeasureSpec是View的內部類,其封裝了一個View的規格尺寸,包括View的寬和高的信息,它的作用是在Measure流程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後在onMeasure方法中根據這個MeasureSpec來確定View的寬和高。
從MeasureSpec的常量可以看出,它代表了32位的int值,其中高2位代表了SpecMode,低30位則代表SpecSize。SpecMode指的是測量模式,SpecSize指的是測量大小。SpecMode有3種模式,如下所示。
- UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用於系統內部的測量。
- AT_MOST:最大模式,對應於wrap_comtent屬性,子View的最終大小是父View指定的SpecSize值,並且子View的大小不能大於這個值。
- EXACTLY:精確模式,對應於 match_parent 屬性和具體的數值,父容器測量出 View所需要的大小,也就是SpecSize的值。
對於每一個View,都持有一個MeasureSpec,而該MeasureSpec則保存了該View的尺寸規格。在View的測量流程中,通過makeMeasureSpec來保存寬和高的信息。通過getMode或getSize得到模式和寬、高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影響的。作爲頂層View的DecorView來說,其並沒有父容器,那麼它的MeasureSpec是如何得來的呢?爲了解決這個疑問,我們再回到ViewRootImpl的PerformTraveals方法,如下所示:
註釋 1 處調用了 getRootMeasureSpec(mWidth,lp.width)方法
getRootMeasureSpec方法的第一個參數windowSize指的是窗口的尺寸,所以對於DecorView來說,它的MeasureSpec由自身的LayoutParams和窗口的尺寸決定,這一點和普通View是不同的。
註釋2處的performMeasure方法
View的measure流程
measure 用來測量 View 的寬和高,它的流程分爲 View 的 measure 流程和 ViewGroup 的measure流程,只不過ViewGroup的measure流程除了要完成自己的測量,還要遍歷地調用子元素的measure()方法。
View的measure流程
View的onMeasure方法
setMeasuredDimension方法
getDefaultSize()方法
根據不同的SpecMode值來返回不同的result值,也就是SpecSize。在AT_MOST和EXACTLY模式下,都返回SpecSize這個值,即View在這兩種模式下的測量寬和高直接取決於SpecSize。也就是說,對於一個直接繼承自View的自定義View來說,它的wrap_content 和 match_parent 屬性的效果是一樣的。
對於一個直接繼承自View的自定義View來說,它的wrap_content 和 match_parent 屬性的效果是一樣的。因此如果要實現自定義 View 的wrap_content,則要重寫onMeasure方法,並對自定義View的wrap_content屬性進行處理。
而在 UNSPECIFIED 模式下返回的是 getDefaultSize 方法的第一個參數 size 的值,size 的值從onMeasure方法來看是getSuggestedMinimumWidth方法或者getSuggestedMinimumHeight方法得到的。
getSuggestedMinimumWidth方法
如果 View 沒有設置背景,則取值爲 mMinWidth,mMinWidth 是可以設置的,它對應於Android:minWidth這個屬性設置的值或者View的setMinimumWidth的值;如果不指定的話,則默認爲0。
總結一下:getSuggestedMinimumWidth方法就是:如果View沒有設置背景,則返回mMinWidth;如果設置了背景,就返回mMinWidth和Drawable的最小寬度之間的最大值。
ViewGroup的measure流程
對於ViewGroup,它不只要測量自身,還要遍歷地調用子元素的measure()方法。ViewGroup中沒有定義onMeasure()方法,但卻定義了measureChildren()方法:
遍歷子元素並調用measureChild方法,measureChild方法如下所示:
調用 child.getLayoutParams()方法來獲得子元素的 LayoutParams 屬性,獲取子元素的MeasureSpec 並調用子元素的 measure()方法進行測量。
getChildMeasureSpec()方法
- 根據父容器的MeasureSpec模式再結合子元素的LayoutParams屬性來得出的子元素的 MeasureSpec 屬性。
- 需要注意的是,如果父容器的 MeasureSpec 屬性爲AT_MOST,子元素的LayoutParams屬性爲WRAP_CONTENT,那根據上面代碼註釋1處的代碼,我們會發現子元素的MeasureSpec屬性也爲AT_MOST,它的SpecSize值爲父容器的SpecSize減去padding的值。換句話說,這和子元素設置LayoutParams屬性爲MATCH_PARENT效果是一樣的。爲了解決這個問題,需要在LayoutParams屬性爲WRAP_CONTENT時指定一下默認的寬和高。
ViewGroup並沒有提供onMeasure 方法,而是讓其子類來各自實現測量的方法,究其原因就是ViewGroup有不同佈局的需要,很難統一。比如ViewGroup的子類LinearLayout的measure流程:
LinearLayout定義mTotalLength用來存儲LinearLayout在垂直方向的高度,然後遍歷子元素,根據子元素的MeasureSpec模式分別計算每個子元素的高度。如果是WRAP_CONTENT,則將每個子元素的高度和margin垂直高度等值相加並賦值給mTotalLength。當然,最後還要加上垂直方向padding的值。如果佈局高度設置爲MATCH_PARENT 或者具體數值,則和View的測量方法是一樣的。
View的layout流程
layout方法的作用是確定元素的位置。ViewGroup中的layout方法用來確定子元素的位置,View中的layout方法則用來確定自身的位置。首先我們看看View的layout方法:
layout方法的4個參數l、t、r、b分別是View從左、上、右、下相對於其父容器的距離。接着來查看setFrame方法裏做了什麼,代碼如下所示:
- setFrame方法用傳進來的l、t、r、b這4個參數分別初始化mLeft、mTop、mRight、mBottom這4個值,這樣就確定了該View在父容器中的位置。在調用setFrame方法後,會調用onLayout方法
onLayout方法是一個空方法,這和onMeasure方法類似。
LinearLayout的onLayout方法:
方法會遍歷子元素並調用setChildFrame方法。其中childTop值是不斷累加的,這樣子元素纔會依次按照垂直方向一個接一個排列下去而不會是重疊的。接着看setChildFrame方法:
在setChildFrame方法中調用子元素的layout方法來確定自己的位置。
View的draw流程
官方註釋清楚地說明了每一步的做法,它們分別是:
- 如果需要,則繪製背景。
- 保存當前canvas層。
- 繪製View的內容。
- 繪製子View。
- 如果需要,則繪製View的褪色邊緣,這類似於陰影效果。
- 繪製裝飾,比如滾動條。
步驟1:繪製背景
從上面代碼註釋1 處可看出繪製背景考慮了偏移參數 scrollX 和scrollY。如果有偏移值不爲0,則會在偏移後的canvas繪製背景。
步驟3:繪製View的內容
步驟3調用了View的onDraw方法。這個方法是一個空實現,因爲不同的View有着不同的內容,這需要我們自己去實現,即在自定義View中重寫該方法來實現:
步驟4:繪製子View
步驟4調用了dispatchDraw方法,這個方法也是一個空實現,如下所示:
ViewGroup重寫了這個方法,緊接着看看ViewGroup的dispatchDraw方法:
源碼很長,這裏截取了關鍵的部分,在 dispatchDraw 方法中對子類 View 進行遍歷,並調用drawChild方法:
這裏主要調用了View的draw方法,代碼如下所示:
在上面代碼註釋1處判斷是否有緩存,如果沒有則正常繪製,如果有則利用緩存顯示。
步驟6:繪製裝飾
繪製裝飾的方法爲View的onDrawForeground方法:
很明顯這個方法用於繪製ScrollBar以及其他的裝飾,並將它們繪製在視圖內容的上層。