今天大概會聊到的範圍
- Intrinsic measurements
今天的標題可能會讓人有點疑惑,但這是我寫出這段 Code 時的第一反應。
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(Color.White)
.padding(4.dp)
) {
Text(text = "Title", style = titleStyle)
Text(text = "Looooooooooooooog Text")
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
Text(text = "GO")
}
}
這是一個 Dialog,在這個 Dialog 中,我希望整個 Dialog 和最寬的 Text 一樣寬,底部的 Button 可以是一個橫向、佔據整個底部的按鈕。直覺上,因為 Button 預期是最寬的狀態,所以我寫了 fillMaxWidth
。但我最後得到的畫面卻是這樣:
按鈕就真的給我 fillMaxWidth
了,但因為沒有任何 constraint ,所以按鈕就一路延伸到和裝置的畫面一樣寬。
要解決這個問題前,要先了解 Compose 在測量 child 的大小時只會測量一次。就如同上一次聊到的一樣,每個 parent 在測量自己的大小前,都會先測量一次 child 的大小。都測量完之後,parent 才會知道自己多大。但這邊就出現了一個問題,因為測量 Button 時,parent 並不知道自己多大,也沒有特別的 constraint 傳給 Button,因此 Button 就回給 parent 自己要要和 max width 一樣大。
當然,我們可以在 parent ( Column
這一層 ) 寫死固定的 width 來設定 Dialog 大小,但這就沒辦法符合動態和最寬的文字一樣寬的需求。
這邊就要介紹到 Intrinsic measurements。Compose 的系統中設計了一個可以讓 parent 可以先知道 child 可能會多大。
每一個 Composable 都會有四個資料
IntrinsicMeasureScope.minIntrinsicWidth
IntrinsicMeasureScope.minIntrinsicHeight
IntrinsicMeasureScope.maxIntrinsicWidth
IntrinsicMeasureScope.maxIntrinsicHeight
( min | max )Intrinsic( Width | Height )
都是在 InstrinsicMeasureScope
上,至於被使用的時機晚點會提到。因為在實際使用時,我們不會需要手動去取得這四個資料。而是透過 IntrinsicSize.(Max | Min)
來表示最大 or 最小
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(Color.White)
.padding(4.dp)
.width(IntrinsicSize.Max) // 將 width 設定為測量前最大的 child 大小
) {
Text(text = "Title", style = titleStyle)
Text(text = "Looooooooooooooog Text")
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
Text(text = "GO")
}
}
當你將 height
/width
等設定成 IntrinsicSize.(Max | Min)
後,實際上會在 modifier chain 中增加一個 IntrinsicSizeModifier
。這個 Modifier 是一個 LayoutModifer,在計算 constraint 時會去參考 composable 提供的 intrinsic measurement。
// 設定 width(IntrinsicSize.Max) 時,就會在 modifier 中增加一個 MaxIntrinsicWidthModifier
private object MaxIntrinsicWidthModifier : IntrinsicSizeModifier {
// modifier 中,會覆寫 constraint 的計算邏輯
override fun MeasureScope.calculateContentConstraints(
measurable: Measurable,
constraints: Constraints
): Constraints {
// 計算時,會參考 measurable 的 intrinsic size
// 就是上面提到的 ( min | max )Intrinsic( Width | Height )
val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
return Constraints.fixedWidth(width)
}
}
最後我們就可以產出一個以內容為大小依據的 Dialog,包含一個填滿 Dialog 的按鈕。
今天實際的 code 其實不會很複雜,邏輯也很好理解。只是研究時發現和之前聊到的 Layout 邏輯整個串在一起,覺得很有趣。Compose 有很好的 render 邏輯,讓 measure 的次數降到最低。Intrinsic measurement 則是在這個結構下產生的好用工具。
Reference: