有時我們需要自己客製 UI 元件,此時就需要懂 View 的繪製原理。在這篇文章會介紹到:
- View 的繪製流程會經過的三個階段
- layoutParams 和 MeasureSpec 在 measure 階段有什麼用處
View 是如何畫出來的?
View 顯示在畫面上需要經過三個階段,就像一般畫畫的過程一樣,我們需要先知道要畫的元件寬高要是多少、應該畫在哪個地方,最後才畫出圖案。因此三個階段分別是:
measure:測量 View 在父 View 中的寬高應該要是多少
layout:決定 View 在父 View 中的位置
draw :將 View 繪製在父 View 上
Measure 流程
在 View 測量自己寬高時會用到父 View 的 MeasureSpec 和自己的 layoutParams 來決定寬高要多少。
LayoutParams
佈局參數,包含 View 的寬高,用來告訴父 View 自己該如何被測量
LayoutParams.MATCH_PARENT:父 View 多大就跟著多大(扣掉 padding)
LayoutParams.WRAP_CONTENT:大小足夠包含自己的內容(含 padding)
MeasureSpec
父 View 測量子 View 的規則,分三種模式:
UNSPECIFIED:父 View還沒測量出確切值之前傳的模式,沒有太大用意
AT_MOST:強制最大只能是特定大小,子 View 的大小不能超過這個大小
EXACTLY:強制子 View 必須要用傳來的確切大小,保證子View內的View必須在這個大小內
父 View 在 measure() 傳來的 widthMeasureSpec, heightMeasureSpec 是個 int 值,包含兩個訊息 SpecMode 和 SpecSize,SpecMode 是 UNSPECIFIED, AT_MOST, EXACTLY 三種模式其中一種,SpecSize 則是父 View 限制子 View 的大小,兩者可以藉由 MeasureSpec.getMode(measureSpec), MeasureSpec.getSize(measureSpec) 解析出來。
View 的 measure 過程
View 在 onMeasure() 藉由父 View 傳來的 MeasureSpec 和自己的 LayoutParams 決定測量出來的大小,當 measure() 回傳後 measuredWidth、measuredHeight 的值就會被設置了,父 View 會執行子 View 的 measure() 不只一次,父 View 為了找出子 View 的大小會先執行一次 measure() 帶 UNSPECIFIED,當發現大小超過父 View 的限制時再執行一次 measure() 帶入 AT_MOST 或 EXACTLY。繼承 View 或 ViewGroup 的類別必須覆寫 onMeasure,在這邊決定 measuredWidth、measuredHeight。
那麼父 View 的 MeasureSpec 又是指什麼呢?其實就是 ViewGroup 的 MeasureSpec
ViewGroup 的 measure 過程
View 階層的根 View 是 DecorView,啟動 Activity 時會將 DecorView 加到 Window 同時建立 RootViewImple,並將 DecorView 設置到 RootViewImpl。View 階層的繪製過程就是從 RootViewImpl 開始的,在 performTraversals()
執行了 performMeasure()
、 performLayout()
、performDraw()
,這三個方法又分別執行了 View 的 measure()
、 layout()
、draw()
。
measure()
在 View 是 final 無法被覆寫,因此實作 ViewGroup 的類別實作測量子 View 寬高的邏輯是在 onMeasure()
實現,例如 LinearLayout、RelativeLayout 等等,他們的 onMeasure 實作都不一樣。
以 LinearLayout 為例:
關鍵在 getChildMeasureSpec
這個方法,這邊依據父 View 的 MeasureSpec 和子 View 的 LayoutParams 決定子 View 的 MeasureSpec
既然 View 的寬高是由父 View 的 MeasureSpec 和自己的 LayoutParams 決定的,那麼最初始的 DecorView 又是怎麼決定大小的呢?從 ViewRootImpl 的 getRootMeasureSpec()
可以看到 MeasureSpec 是以 WindowManager.LayoutParams 決定,WindowManager.LayoutParams 的初始值是 LayoutParams.MATCH_PARENT
總結
研究到這邊,對於 measure 階段如何測量 View 寬高有基本的理解了,如果自製的 View 包含了某個 TextView 時,想要將 TextView 的位置置中的話,就該用 measured width 來調整,因為在 onLayout 時整個 View 已經測量過寬高了。
以上是自己研究 View 如何實作 measure 所做的整理 希望對於想了解 View 寬高是如何決定的人能夠一些幫助,或者文章中有任何錯誤的地方也歡迎指正~
參考連結