View 原理 — MeasureSpec & LayoutParams

York
6 min readNov 18, 2020

--

有時我們需要自己客製 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 寬高是如何決定的人能夠一些幫助,或者文章中有任何錯誤的地方也歡迎指正~

參考連結

--

--