iT邦幫忙

2021 iThome 鐵人賽

DAY 6
2
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 6

D06 / 為什麼 Modifier 的順序不能亂寫 - Modifier

  • 分享至 

  • xImage
  •  

今天大概會聊到的範圍

  • 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)
) { ... }

.then & composed

在背後,我們可以看到實際上 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 往下運作。(這邊有一個很好的文章說明這件事)

composed

另一種是類似 .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。

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
https://ithelp.ithome.com.tw/upload/images/20210921/20141597yjRZBZKxOW.png https://ithelp.ithome.com.tw/upload/images/20210921/20141597pXIMfxd9pw.png

paddingsize 的順序不同時,兩個 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
    
    // ...
}

我們可以觀察到幾個重點:

  1. PaddingModifier 是一個 LayoutModifier 的實作
  2. LayoutModifier 是一個 interface, 在這個 interface 中,有一個抽象的 MeasureScope extension function measure
  3. measure() 會需要回應 MeasureResultMeasureResult 可以透過 layout() 產生
  4. meaure() 接收兩個參數: measurable, constraints
  5. layout() 的 trailing lambda 是 PlacementScope 為 this

整個運作邏輯會是這樣

  1. 每個 LayoutModifier 都需要覆寫一個 MeasureScope 的 extension function measure()
  2. measure() 最主要的任務就是量出自己大小,透過 MeasureResult 回傳
  3. 在測量自己大小時,會取得一個 measurable,代表目前這個 modifier 所包含的內容(通常是指正在 modify 的物件),measurable 是還不知道大小的。
  4. 同時,還會取得一個 constraint。constraints 代表著 Modifier chain 中,前面的 Modifier 所留下來的限制(最高多高、最寬多寬、位置 ...etc)
  5. 可以透過 measurable 自身的 measure 測量他的大小,測量時要帶入 constraint (這個概念就類似 child view 不能長超過 parent view 的範圍)
  6. 測量後就會是一個 "已知大小但不知位置,可以用來擺放的" Placable
  7. Placable 只能在 PlacementScope 中進行 "擺放 (place)"
  8. layout() 的 trailing lambda 就是 PlacementScope,在這邊進行 place 後,就可以取得 MeasureResult (量好 child 的大小並且擺好後,parent 就知道自己實際需要多大的空間)

LayoutModifier 的運作邏輯和一般的 Compose Layout 邏輯相同,可以簡化成先量 child、再擺 child,都擺放好了之後將自己的大小回傳給自己的 parent。

知道這個概念之後,我們再回頭來看看上面這個 Modifier chain:

https://ithelp.ithome.com.tw/upload/images/20210921/20141597H8OiWbuuaA.png

首先,在一切開始前我們的長、寬的 constraint 就是 max。接下來我們將 constraint 往下丟到下一個 LayoutModifier size()size() 會限制長寬都只有 40dp,當他要 measure 他的 child 時,他會將長、寬40dp 這個 constraint 往下傳遞,依此類對一路到最底層的元件 Box。在 Box place 好後,就會一路將 place 好的結果往上傳。過程中 background、border 這些需要在畫面上進行繪製的 Modifier 也會因為已經 measure 好了知道空間大小後開始進行繪製。

https://ithelp.ithome.com.tw/upload/images/20210921/20141597jmSEGYNrCc.png

附圖是之前說明這段時做的 slide, 和整體文章風格有點不同請見諒


Modifier 終究是 Compose 中的一個重要元素,看這個主題看很久了但仍然覺得今天的文章寫得很紊亂。也許有機會可以再回頭將 Modifier 的運作寫得更詳細、更簡單易懂。


Reference:


上一篇
D05 / 為什麼不會填錯資料? - Inline class, Scope  & DSL design in compose
下一篇
D07 / 怎麼顯示大量資料 - Lazy composables ( LazyColumn & StickyHeader )
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言