Sitemap

Android View — Measure

8 min readJun 20, 2023

--

Press enter or click to view image in full size

前言

當我們將一個元件放入 layout 時,系統是如何判定放置的方位與大小的呢?我們只要透過 xml 的拖移擺動就能完成設置,這中間到底發生了什麼事情?這系列文章就是要來帶大家了解一下,View 與 ViewGroup 之間的繪製邏輯與過程

繪製過程會經歷 Measure 測量、Layout 佈局、Draw 繪製。就從測量開始

Measure

是用來決定一個 View 所期望的大小。因為很多時候在開發上,我們都會有些情況會導致,View 被其他元件給擠壓或是被佈局所影響,讓大小與我們先前所預期的有落差,而這預期就是先在 Measure 中被定義。

誰發起的測量工作 ?

測量工作是由父容器觸發並啟動的。當父容器需要佈局和繪製其子 View 時,它會調用子 View 的 measure() 方法來觸發測量過程。當一個 View 接收到測量請求時,會執行測量過程來確定自身的期望尺寸。

Get Voss’s stories in your inbox

Join Medium for free to get updates from this writer.

具體而言,當父容器的 measure() 方法被調用時,它會依次調用每個子 View 的 measure() 方法,從而觸發子 View 的測量工作。measure() 是一個呼叫函數,真正執行測量工作的,是在 View 中的 onMeasure 方法。

Press enter or click to view image in full size

測量過程

測量計算尺寸的過程大致上可以分為三個階段

第一階段:獲取測量規格( MeasureSpec )

在測量過程開始之前,View 會獲取由父容器傳遞的測量規格。測量規格由兩個主要部分組成:

測量模式(MeasureSpec mode)和測量尺寸(MeasureSpec size)

可以在 View 中的 onMeasure 的參數看見,兩個 MeasureSpec 代表著寬高

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

}

測量模式( MeasureSpec mode )

用於確定 View 應該如何測量自身的尺寸。測量模式有三種可能的值:

  • EXACTLY:View 的尺寸應該精確地匹配測量尺寸指定的數值。例如,當 View 的寬度設置為固定值或為 match_parent 時
  • AT_MOST:View 的尺寸應該不超過測量尺寸指定的數值。例如,當 View 的寬度設置為 wrap_content 時
  • UNSPECIFIED:View 的尺寸沒有限制。這種模式很少在實際使用中出現

如果想在 CustomView 中,修改測量規格。可以更換 onMeasure 傳入的參數

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 修改测量模式
val newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)

// 調用父類的 onMeasure() 方法,傳遞修改後的測量規格
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec)
}

測量尺寸(MeasureSpec size)

指定 View 應該如何測量自身的尺寸值。尺寸值的具體含義根據測量模式的不同而有所差異。

第二階段:計算期望尺寸

根據獲取到的測量規格,View 根據自身特性和佈局參數來計算出期望尺寸

  • EXACTLY:View 的期望尺寸就是測量尺寸的值,即為 size
  • AT_MOST : View 的期望尺寸應該不超過測量尺寸的值,但可以是任意小於等於該值的尺寸。在這種情況下,View 需要根據自身的內容和佈局參數來決定期望尺寸的大小
  • UNSPECIFIED : View 的期望尺寸可以是任意大小,通常用於特定情況下的特殊測量需求

最後階段:設置期望尺寸

View 在計算出期望尺寸後,會使用 setMeasuredDimension(width: Int, height: Int) 方法來設置自身的期望寬度和高度。這個方法將期望尺寸存儲在 View 的內部,以供後續的佈局和繪製階段使用。所以我們想要自行修改 measure 中的測量邏輯,記得在計算完後,要將最新的結果透過setMeasureDimension 保存下來。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

// 取得目前的測量結果
var width = measuredWidth
var height = measuredHeight

// 對目前的結果進行加工
// do something ...

// 將最新的結果透過 setMeasureDimension 保存下去
setMeasuredDimension(width, height)
}

小結

以上介紹完了測量的整體過程,來總結一下整個流程:

  1. 父層調用子 View 的 measure() 方法:父層通過遍歷子 View 的集合,逐個調用每個子 View 的 measure() 方法來觸發測量過程
  2. 子 View 自身看摋執行測量:在 onMeasure() 方法中,根據自身的佈局參數、內容和其他因素進行測量計算,以確定期望尺寸
  3. 子 View 保存測量結果:使用 setMeasuredDimension() 方法將測量的寬度和高度保存下來
  4. 父層獲取子 View 的測量結果:父層在調用完所有子 View 的 measure() 方法後,通過獲取每個子 View 的測量結果來確定子 View 的佈局位置和大小
  5. 父層計算自身的測量結果:根據子 View 的測量結果以及父容器自身的佈局規則,計算自己的期望尺寸,將其保存為測量結果
  6. 父層傳遞測量結果給上層:如果當前的 View 是嵌套在另一個父層中,那麼父層會將自己的測量結果傳遞給上層,從而形成遞歸的測量過程,直到最頂層的容器

由此可知,測量是從最上層發起,一直往下測量到最深層的子 View,直到所有子 View 的測量結束後,再將這些結果不斷往上傳遞到最終的容器,才得以測量出我們所看到 layout 中的所有元件大小與佈局。

應用範例

上面已經大概知道了,整體的測量過程,但可能太多文字上的敘述,很難讓人理解。這邊筆者就提供一個簡單的範例,讓大家試試身手。

目標是做出一個正方形的 ImageView,讓不論寬高大小如何,都會去相互對應並最終會是一個正方形,程式碼如下:

class SquareImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AppCompatImageView(context, attrs, defStyleAttr) {


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

// 取得目前測量結果
var width = measuredWidth
var height = measuredHeight

// 加工測量結果
if (measuredWidth > measuredHeight) {
width = measuredHeight
} else {
height = measuredWidth
}
// 將加工結果設置
setMeasuredDimension(width, height)
}
}

延伸閱讀

--

--