D06 / 为什麽 Modifier 的顺序不能乱写 - Modifier

今天大概会聊到的范围

  • 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:


<<:  Day20. Blue Prism 的烤肉串-BP 串Objects页流程传递参数

>>:  [Day 15] Leetcode 138. Copy List with Random Pointer (C++)

CompTIA SY0-601 Braindumps - All About The SY0-601 Exam

Most of the aspiring professionals are getting cer...

Golang-sync.Map 高并发的神之好用方法

最近收到了一个需求 需要不断的在一个data pool随机找到资料後,给前端显示新value 刚开始...

musl libc 简介与其 porting(六)Busy Box Band

这篇我们将正式进入libc testing的部份,我们可以用以下方式来运行libc-test、并使用...

30天完成家庭任务平台:第三十天

终於最後一天了!!~虽然没有写的很好,就是心得的小小整理,但是也坚持了三十天了,谢谢不小心进到这个网...

Day 03-Terraform State 之你的 Local State 不是我的 State

Terraform State 之你的 Local State 不是我的 State State 是...