今天大概會聊到的範圍
- Modifier 的運作
Modifier 是我們在 Compose 系統中,最廣泛使用於調整 Composable 顯示行為的工具。在這一集Android Developer Backstage podcast中有聊到: Compose 中,參數通常代表 composable 的內容,Modifier 通常代表 composable 的行為。
在使用 Modifier 時,我們通常會串接一個或多個 Modifier。串連 Modifier 時,通常我們會直接用 chaining 的寫法:
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.background(color = Color.White)
.padding(8.dp)
) { ... }
在背後,我們可以看到實際上 Modifier 大多都是用兩種方式在進行串連。
第一種是 .then()
,then
是 Modifier 的一個 function, 用於串連不同的 Modifier。以 padding
為例,在 padding 的實作中,就是使用了 then
將實際效果的 PaddingModifier
與 Modifier chain 中的其他 Modifier 串在一起。
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(...)
)
then 的效果是將既有的 Modifier (當下的 this) 與參數中的 Modifier (稱為 other)合併成一個 CombinedModifier
。
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
每一個 CombinedModifier
都會紀錄產生時的 this 與 other。在 Modifier chain 的下一步,CombinedModifier 又會與下一個 Modifier 合併成新的 CombinedModifier,最後在解析時會一層一層的往上解析,從每一層的 other 開始解析到最頂層,最後再從每一層的 this 往下運作。(這邊有一個很好的文章說明這件事)
另一種是類似 .border()
,使用了一個 function composed()
。composed 會接受一個 factory lambda 以及一個 inspectorInfo
。
fun Modifier.border(width: Dp, brush: Brush, shape: Shape): Modifier = composed(
factory = {
// ...
this.then(
Modifier.drawWithCache { ... }
)
},
inspectorInfo = ...
)
讓我們來看看 composed
這個 function
fun Modifier.composed(
inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
factory: @Composable Modifier.() -> Modifier
): Modifier = this.then(ComposedModifier(inspectorInfo, factory))
其實 factory 本身也是一個 composable,並且是一個以 Modifier 為 reciever 的 lambda。最後,一樣會透過 then 去串接 ComposedModifier 和 Modifier chain 上的其他 modifier。
inspectorInfo 是為了讓 preview 等工具可以觀測到這個 Modifier 所做的操作。factory 則是可以讓 Modifier 可以利用 remember
之類的方式,將 state 記錄下來,產生 stateful 的 Modifier。
最後讓我們看看這兩個 composable
// compose 1
Box(
modifier = Modifier
.border(1.dp, color = Color.Red)
.size(40.dp) /// <<<<
.background(color = Color.White)
.padding(12.dp) /// <<<<<
.border(1.dp, color = Color.Blue)
)
// compose 2
Box(
modifier = Modifier
.border(1.dp, color = Color.Red)
.padding(12.dp) /// <<<<<
.background(color = Color.White)
.size(40.dp) /// <<<<
.border(1.dp, color = Color.Blue)
)
compose 1 | compose 2 |
---|---|
padding
和 size
的順序不同時,兩個 Modifier chain 產生的結果並不相同。要理解這個現象,我們得先來看看剛剛提到的 PaddingModifier
:
private class PaddingModifier(...) :
LayoutModifier, InspectorValueInfo(inspectorInfo) {
// ...
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// ...
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
// ...
return layout(width, height) {
// ...
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
interface LayoutModifier : Modifier.Element {
fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult
// ...
}
我們可以觀察到幾個重點:
PaddingModifier
是一個 LayoutModifier
的實作LayoutModifier
是一個 interface, 在這個 interface 中,有一個抽象的 MeasureScope
extension function measure
measure()
會需要回應 MeasureResult
,MeasureResult
可以透過 layout()
產生meaure()
接收兩個參數: measurable, constraintslayout()
的 trailing lambda 是 PlacementScope
為 this整個運作邏輯會是這樣
LayoutModifier
都需要覆寫一個 MeasureScope
的 extension function measure()
measure()
最主要的任務就是量出自己大小,透過 MeasureResult
回傳Placable
Placable
只能在 PlacementScope
中進行 "擺放 (place)"layout()
的 trailing lambda 就是 PlacementScope
,在這邊進行 place 後,就可以取得 MeasureResult
(量好 child 的大小並且擺好後,parent 就知道自己實際需要多大的空間)LayoutModifier
的運作邏輯和一般的 Compose Layout 邏輯相同,可以簡化成先量 child、再擺 child,都擺放好了之後將自己的大小回傳給自己的 parent。
知道這個概念之後,我們再回頭來看看上面這個 Modifier chain:
首先,在一切開始前我們的長、寬的 constraint 就是 max。接下來我們將 constraint 往下丟到下一個 LayoutModifier size()
。size()
會限制長寬都只有 40dp,當他要 measure 他的 child 時,他會將長、寬40dp 這個 constraint 往下傳遞,依此類對一路到最底層的元件 Box。在 Box place 好後,就會一路將 place 好的結果往上傳。過程中 background、border 這些需要在畫面上進行繪製的 Modifier 也會因為已經 measure 好了知道空間大小後開始進行繪製。
附圖是之前說明這段時做的 slide, 和整體文章風格有點不同請見諒
Modifier 終究是 Compose 中的一個重要元素,看這個主題看很久了但仍然覺得今天的文章寫得很紊亂。也許有機會可以再回頭將 Modifier 的運作寫得更詳細、更簡單易懂。
Reference: