文章

Jetpack Compose 笔记(6) - 硬核深入 Modifier

⚠️ 警告:这一节不是罗列各种 Modifier 的使用与效果,而是深入它们的原理,不适合快餐式学习。 只想学简单用法的建议看文档或随便一个免费的视频教程。

⚠️ 警告:这一节非常硬核,我花了三天学习,所以不建议尝试短时间看完并理解。

本节源码参照 Compose 1.3.0-alpha02

回顾

虽然还没有系统地学习过,对 Modifier 我们也不陌生,它用来给组件设置各种属性与监听。对于 Modifier 和直接传参也做过简要的总结:前者多用于通用属性,后者多用于特有属性。

多个 Modifer 有先后关系,典型例子是 Modifier.padding() 一个函数,根据调用顺序不同,可以实现 marginpadding 两个属性的功能。

Modifier 是什么

就是字面意思「是什么」。

接口与单例实现

跟踪进源码不难发现,它既是接口,又是单例实现。单例实现利用了 kotlin 语法 companion object : Modifier。严格意义上,若想使用这个单例得这样写:Modifier.Companion.padding(),代表 Modifier 这个接口的伴生对象。幸运的是 kotlin 支持缩写,因此可以简单地写成 Modifier.padding()。事实上使用伴生对象正是为了能够以「与接口同名」的方式调用,让代码读起来更自然。

除此之外这个单例实现就是个废物, 仅作为占位符使用。占位符?自定义 Compose 时,通常希望提供一个接口给外部以便控制样式,那么就要这么写:

@Composable
fun Custom(modifier: Modifier) {
    Box(modifier)
}

// 使用
Custom(modifier = Modifier.size(60.dp))

那如果 caller 不想特别指定任何值怎么办?那我们给一个「占位符」:

@Composable
fun Custom(modifier: Modifier = Modifier) {...}

⬆️ 这是自定义 Compose 中的常见写法,并且建议将 modifier 作为第一个有默认值的参数。

注意 modifier: Modifier = Modifier 这里,两个 Modifier 不是一个东西,前者是接口定义,后者是伴生对象的单例实现,它等价于这么写:modifier: Modifier = Modifier.Companion

CombinedModifier

每一种 Modifier (padding, background...) 都是一个实现类。比较特殊的除了单例实现外,就是 CombinedModifier,顾名思义,它是把两个 Modifier 组合在一起,没错,叒套娃 🪆,这个套娃由 Modifier.then() 方法创建,先加入的 Modifier 作为 outer,后加入的作为 inner

例如调用 Modifier.padding(8.dp).size(60.dp),就会创建一个 CombinedModifier,它的 outerPaddingModifierinnerSizeModifier

class CombinedModifier(private val outer: Modifier,private val inner: Modifier) : Modifier {
    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.any(predicate) || inner.any(predicate)

    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.all(predicate) && inner.all(predicate)
    // others...
}

通过对 anyall 两个函数的复写就能看出套娃的本质。any 是判断是否包含符合条件的 Modifier,all 则是判断是否所有 Modifier 都符合条件。

举个例子:

Modifier.padding(8.dp).size(60.dp).padding(4.dp) 返回值如下:

CombinedModifier(
  outer = CombinedModifier(
            outer = PaddingModifier(8.dp),
            inner = SizeModifier(60.dp),
          ),
  inner = PaddingModifier(4.dp),
)

Element

除了单例实现与 CombinedModifier,其他 Modifier 其实是实现了子接口 Element,这个子接口也是个废物。Element 的存在是为了提供四个方法的默认实现避免编写重复代码:foldIn, foldOut, any, all。这四个家伙只为了 CombinedModifier 而诞生,对于其他实现类来说没啥意义。不妨再解释一下这四个方法的作用:

  • foldIn(): 从外层(先加入的 Modifier)开始,依次应用到初始对象 initial: R 上,返回应用后的对象。
  • foldOut(): 从内层(后加入的 Modifier)开始,依次应用到初始对象 initial: R 上,返回应用后的对象。
  • any(): 自己或自己所包含的 Modifier 是否有一个符合给定条件。
  • all(): 自己以及自己所包含的 Modifier 是否全部符合给定条件。

例如 all(),对于一个普通的实现类,它不包含其他 Modifier,自己符合条件就符合,自己不符合就不符合,何来「全部」之说?其他三个同理。所以后面分析时,我们可以忽略 Element 了。

ComposedModifier

ComposedModifier 的最简用法如下:

val modifier = Modifier.composed { Modifier.padding(8.dp) }
// 然后就可以像普通 Modifier 那样使用。

ComposedModifier 实现的是 Element 接口,所以某种程度上来讲,它没有前几个那么「特殊」,但也很特殊。它同样不提供任何功能,而是工厂模式的实现 —— 把另一个 Modifier 包在一个工厂函数里。在自己真正被使用时才会执行这个工厂函数创建出实际的 Modifier。

什么叫被真正使用? 随便跟踪一个接收 Modifier 的 Compose 函数,最终可以找到一个扩展函数:Composer.materialize(modifier: Modifier),它的内部代码如下:

// Composer.materialize(modifier: Modifier)
// 删除了一些代码
val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
    acc.then(
        if (element is ComposedModifier) {
            val factory = element.factory as Modifier.(Composer, Int) -> Modifier
            val composedMod = factory(Modifier, this, 0) // 执行工厂函数得到具体的 Modifier
            materialize(composedMod)
        } else {
            newElement
        }
    )
}

可以看出,这个函数遍历 Modifier 然后创建了一串新的 Modifier,这不是多此一举吗?关键在于这个过程中递归执行了 ComposedModifier 的工厂函数,用生成的 Modifier 替换原先的 ComposedModifier,这就是所谓「被真正使用」的时候。

那... 有啥用? Modifier.composed() 的注释给了我们提示:它用于创建独立的有状态的 Modifier。所谓「独立」就是它会生成多个 Modifier 对象,互不影响。这里的「状态」和前几节讨论的 Compose 本身的「状态」是一个意思。比如 Modifier.padding(8.dp),它的状态是 8dp,但这个值只是一个外部传递的参数,不能算作它内部的状态(PaddingModifier 没有能力维护/修改这个值)。此时用 ComposedModifier 的话可以这么写:

val modifier = Modifier.composed {
  var padding by remember { mutableStateOf(8.dp) }
  Modifier.padding(padding).clickable { padding = 0.dp  }
}
Text("chenhe", modifier)

这里 padding 就成了名副其实的内部状态,并且可以修改它了。

此时好事者会排出一个类似的代码,质疑这两种写法有什么区别:

// 另一种写法
var padding by remember { mutableStateOf(8.dp) }
val modifier = Modifier.padding(padding).clickable { padding = 0.dp  }
Text("chenhe", modifier)

在这个用例下的确没区别,但如果把 modifier 应用到多个组件中,前者 ComposedModifier 的写法可以保持独立,即某一个组件点击后 padding 变成 0,其他的依然是初始值 8。后一种写法所有组件共享状态,点击某一组件后所有组件的 padding 都会变成 0。

道理懂了,那使用场景呢?

像上面那种演示写法实际项目中不会出现的,这种需求我们宁愿显式创建多个普通的 Modifier,这样更清晰。ComposedModifier 主要用途是:

  • 封装带状态的 Modifier。 比如现在想把上面的演示写法封装进一个自定义 Modifier 方法 Modifier.clickToClearPaddingModifier() 那就只能这么写。
  • 封装的 Modifier 需要 Compose 环境。 有时候我们想要的并不是状态,而仅仅是一个 Compose 执行环境,比如希望使用 LaunchedEffect() 方法或者读取 ComposiltionLocal 值的时候,Modifier.composed() 接收的 lambda 自带 @Composable 注解。

比如官方的 Modifier.animateEnterExit() 就利用 ComposedModifier 来获得 Compose 环境,从而可以调用其他 Compose 方法。

LayoutModifier

上面已经研究完了特殊的 Modifier,现在开始研究有实际意义的那些。首当其冲的是 LayoutModifier,它是一个接口,与布局相关,padding, size 等都实现了它。

工作原理

先看一个现象,如果多次调用 Modifier.size(),最终的值是多少呢?

@Composable
fun Custom(modifier: Modifier = Modifier) {
    Box(modifier.background(Color.Green).size(40.dp))
}
// 使用
Custom(modifier = Modifier.size(200.dp))

虽然看起来后调用 40 会覆盖先调用 200,但事实相反,最终值是 200。要搞清楚这个,就得大致梳理下测量时 LayoutModifier 如何发挥作用。

事实上,Compose 每一个组件最终都会生成 LayoutNode 对象,作为这个组件的代表存在内存中。它们的测量与布局由 LayoutNode.remeasure()LayoutNode.replace() 方法负责,其实前者已经做完了所有工作,包括布局的计算,后者只是用计算的结果布局一下。那我们跟踪一下 LayoutNode.remeasure() 方法,发现最终调用的是 LayoutNodeLayoutDelegate.performMeasure():

// LayoutNodeLayoutDelegate
private fun performMeasure(constraints: Constraints) {
  check(layoutState == LayoutState.Idle) {
    "layout state is not idle before measure starts"
  }
  layoutState = LayoutState.Measuring
  measurePending = false
  layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
    layoutNode,
    affectsLookahead = false
  ) {
    outerWrapper.measure(constraints) // 关键看这一行
  }
  // ... 省略一些代码
}

LayoutNodeLayoutDelegate 是真正负责测量的对象,Google 团队把这块逻辑抽了出去以避免 LayoutNode 代码太多(看看 View 的源码你会深有体会)。outerWrapper 是啥?我们得研究下操作这个 Delegate 的地方。

LayoutNode.modifier 储存了经过预处理(解析 ComposedModifier 等工作)的所有添加到这个组件的 Modifier(若有多个则以 CombinedModifier 的套娃形式保存),看看它的 setter:

// LayoutNode
// 省略部分代码
internal val innerLayoutNodeWrapper = InnerPlaceable(this)
override var modifier: Modifier = Modifier
    set(value) {
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
            // mod 是遍历到的那一个 Modifier,toWrap 是上一轮应用的结果(或初始值)
            var wrapper = toWrap
            if (mod is LayoutModifier) {
                wrapper = ModifiedLayoutNode(toWrap, mod).apply {
                  onInitialize()
                  updateLookaheadScope(mLookaheadScope)
                }
            }
            // ... other modifier branches
            wrapper // ^foldOut
        }
        outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
        layoutDelegate.outerWrapper = outerWrapper
    }

关键看这个 modifier.foldOut(),它是从初始值开始由内向外(从后添加的向先添加的)应用一系列的 Modifier 并返回最终值,中间每一次应用的结果都作为下一次的初始值。最终结果一股脑塞给了 layoutDelegate.outerWrapper,它的默认值是 LayoutNode.innerLayoutNodeWrapper,现在被替换成了包裹了一系列 Modifier 的 outWrapper。注意,outWrapper 包裹的是最外层,也就是最先添加的 Modifier。

看一个例子:

Box(Modifier.padding(10.dp).padding(20.dp))
生成的 outerWrapper 是这样的:
ModifiedLayoutNode(
  modifier = PaddingModifier(10.dp),
  wrapped = ModifiedLayoutWrapper(
    modifier = PaddingModifier(20.dp),
    wrapped = InnerPlaceable
  )
)

那么继续看 ModifiedLayoutNode.measure() 方法:

// ModifiedLayoutNode
override fun measure(constraints: Constraints): Placeable {
  performingMeasure(constraints) {
    with(modifier) { // 这个 modifier 是 LayoutModifier
      measureResult = measure(wrapped, constraints) // 处于 LayoutModifier 上下文内
      this@ModifiedLayoutNode
    }
  }
  onMeasured()
  return this
}

内部调用了 LayoutModifier 接口内的 MeasureScope.measure() 方法,显然,每一个 LayoutModifier 的实现类都一定会实现这个方法。这里传入的参数 wrapped 来自构造函数,在 LayoutNode.modifier 的 setter 中传入的,就是内层的 ModifiedLayoutNode(或初始值 innerLayoutNodeWrapper)。

到此就可以串起来了,最先被调用的是 outWrapper.measure(),它内部再调用对应 Modifier 的 measure(measurable: Measurable, constraints: Constraints) 方法,传入内层的 ModifiedLayoutNode 作为参数。这个方法的具体实现中肯定要先调用 measurable.measure() 再在返回值的基础上修改。所以真正先执行测量的是内部 Modifier(也就是后添加的)。 先应用的 size 覆盖了后应用的也就不足为奇了。

小测验

Box(modifier.padding(10.dp).padding(40.dp))

执行结果是什么?

这里就不是覆盖了,具体什么行为要看 Modifier 怎么对待内部的测量结果。PaddingModifier 选择在内部结果的基础上添加边距,所以执行结果是添加了 50dp 的边距。

Modifier.layout()

LayoutModifierImplModifier.layout() 的返回值,从名字就能看出它是 LayoutModifier 接口的最基本实现,可以调整这个组件的位置与尺寸。

Andriod 传统 UI 系统中,自定义 View 有三大块:

  • 自定义布局
  • 自定义绘制
  • 自定义触摸

在 Compose 中虽然没有了 View 的概念,但自定义的概念与需求依然存在。Modifier.layout() 方法就与「自定义布局」息息相关。

Modifier.layout() 的用处是修改对应组件的尺寸与位置偏移。注意,只是修改,不能完全自定义。 先来看一下基本用法,实现与默认情况一致的布局效果:

Text(text = "Chenhe", Modifier.layout { measurable, constraints ->
    val placeable = measurable.measure(constraints) // 测量自己,此时可以修改限制
    layout(placeable.width, placeable.height) { // 布局自己,此时可以传入修改后的尺寸
        placeable.placeRelative(0, 0) // 设置位置偏移 placeRelative 可自适应 RTL
    }
})

和传统的 View 系统很像,这里包含两个步骤:使用父组件给的限制测量自己,然后布局一下自己。里面的 layout() 函数是创建 MeasureResult 的便捷写法。既然这样,就做一下类比。

功能 自由度
Modifier.layout() 测量自己 只能修改标准测量的结果
View.onMeasure() 测量子 View,然后测量自己。 可以完全自己计算宽高
layout 中的 placementBlock 参数 设置自己的位置偏移 只能设置整体偏移,不能布局子组件
View.onLayout() 具体布局子 View 随意布局子 View

可以看出 Modifier.layout() 不如传统的自定义 View 灵活。这很好理解,Modifier 终究只是一个附加的东西,它不是组件自己,也无法获取组件的各种参数,得不到参数又怎样进行完全自定义的计算呢?而 View 不同,它的测量与布局方法就写在自己的类里,完全有能力根据各种参数以及子 View 的各种参数来计算。

所以 LayoutModifierImpl 不能实现完整的自定义布局,它适用于给组件增加装饰效果。 所谓「装饰效果」就是不干涉组件内部测量布局,只在外部调整,例如 padding。

如果想修改组件内部效果,唯一的办法是改组件源码。如果组件不是自己写的,那只能把它拷贝一份来修改。注意函数不是类,不能像 View 那样继承。

Modifier.requiredSize()

除了 size,还有 requiredSize,它将忽略左侧设置的 size 固执地按照自己的想法来测量,但它不能左右最终大小

Custom(modifier = Modifier.size(120.dp).requiredSize(60.dp))

这个例子中 size 给内层测量的限制是 120 并自己测量为 120,但内层是一个固执的 requiredSize,它偏要测量为 60。那么视觉上只有 60,但实际上是 120,效果是四周都是空白,如图:

Custom(modifier = Modifier.size(60.dp).requiredSize(120.dp))

反过来。size 给内部的限制是 60,requiredSize 再一次固执地按照 120 测量。那么这个组件按照 120 绘制,但显示出来的时候只有 60 的那一部分。假设这是一个图片,结果就是被裁剪了。

DrawModifier

LayoutModifier 与布局相关,DrawModifier 则与绘制相关。background 等是它的实现。

工作原理

处理

要知道工作原理,就得找到这类 Modifier 在哪被处理的。其实也在 LayoutNode.modifier 的 setter 里:

// LayoutNode
// 省略部分代码
internal val innerLayoutNodeWrapper = InnerPlaceable(this)
override var modifier: Modifier = Modifier
    set(value) {
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod: Modifier.Element, toWrap:LayoutNodeWrapper ->
            // mod 是遍历到的那一个 Modifier,toWrap 是上一轮应用的结果(或初始值)
            toWrap.entities.addBeforeLayoutModifier(toWrap, mod) // <--- 看这一行
            // ...
        }
        outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
        layoutDelegate.outerWrapper = outerWrapper
    }

上面分析 LayoutModifier 工作原理时已经看过这段代码,toWrap 要么是包裹了 LayoutModifierModifiedLayoutNode,要么是初始的负责组件的内部测量的 InnerPlaceable

DrawModifier 是在 toWrap.entities.addBeforeLayoutModifier(toWrap, mod) 中处理的,它究竟干了什么呢?也是把 Modifier 包装一层,只不过这一次用的是 LayoutNodeEntity 的子类,而是不 ModifiedLayoutNode 了。

graph TB
s[遍历 Modifier] --mod,toWrap--> jt[/判断 mod 类型/]
jt --LayoutModifier--> pl[用 ModifiedLayoutNode 包装] ---> 基于包装结果执行下一轮遍历
jt --四种 Modifier--> pe[用 LayoutNodeEntity 包装] ---> pee[把包装结果添加到 toWrap.entities 中]
jt --其他 Modifier --> ...

上图中的「四种 Modifier」包括:DrawModifier / PointerInputModifier / SemanticsModifier / ParentDataModifier

toWrap.entitiesEntityList,其内部的 entities 才是真正的储存结构,它是多个链表的头节点组成的数组,每一个元素是一个链表,每一个链表保存着一类 Modifier 的包装,「把包装结果添加到 toWrap.entities 中」具体来是用头插法加入到对应链表中。先添加的 Modifer 在外层,这里是用从内向外遍历,使用头插法的结果就是链表里的顺序与添加的顺序一致:先添加(左边)的 DrawModifier 在链头。示意图如下:

class EntityList {
  val entities = [
    0: DrawEntity(DrawModifier1) -> DrawEntity(DrawModifier2) -> ...,
    1: PointerInputEntity(PointerInputModifier) -> ...,
    2: SemanticsEntity(SemanticsEntity) -> ...,
    3: SimpleEntity(ParentDataModifier) -> ...,
    4: SimpleEntity(OnPlacedModifier) -> ...,
    5: SimpleEntity(OnRemeasuredModifier) -> ...,
    6: SimpleEntity(LookaheadOnPlacedModifier) -> ...,
  ]
}

这一段很容易理解,看不懂的需要补一下数据结构知识。

至此我们知道了所有 DrawModifier 都被包装后装进了 LayoutNodeWrapper 里保存。那什么时候具体使用呢?

调用

当然是绘制的时候调用啦。上面提到测量与布局由 LayoutNode.remeasure()LayoutNode.replace() 方法负责,绘制则是由 LayoutNode.draw() 方法负责。它的实现非常简单:

// LayoutNode
internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)

outerLayoutNodeWrapper 是啥前面也分析过了,忘记的同学复习一下 LayoutModifier 工作原理。简单回顾,它是最外侧的 LayoutModifier 的包装,如果没有的话就是初始的 innerLayoutNodeWrapper = InnerPlaceable(this)。这里就看它们的公共父类中的方法源码:

// LayoutNodeWrapper
/** Draws the content of the LayoutNode */
fun draw(canvas: Canvas) {
  val layer = layer
  if (layer != null) {
    layer.drawLayer(canvas)
  } else {
    val x = position.x.toFloat()
    val y = position.y.toFloat()
    canvas.translate(x, y)
    drawContainedDrawModifiers(canvas)
    canvas.translate(-x, -y)
  }
}

注意:这里的 layer 与传统 View 系统中的 layer 以及传统 Canvas 中的 layer 都没有关系!

背景知识

Android 上任何 UI 框架都逃不过原生 View 系统,Compose 也不例外,也只是对 View 的封装。具体来讲,Compose 在 Android 中以 ComposeView 的形式存在,它是传统 View 的子类。从这个角度,Compose 干的事情就是在一个 View 中绘制出 Compose 的组件树,并自行处理不同组件的触摸。

因为所有组件都在一个 View 中绘制,就难以通过原生 View API 实现透明度、裁剪等功能,否则会影响整个 Compose 而不是某个组件。为了解决这个问题,额外的抽象层 layer 应运而生。

既然 layer 是个抽象层,在不同系统的具体实现自然也不一样。在 Android 中,>= API 29 时使用 RenderNode 实现,< API 29 时尝试使用 RenderNode,失败则回退使用独立的 View 实现。

Compose 中的 layer 有两个作用:

  1. 类似 View.setLayerType(),可以独立绘制与刷新,某些情况下(比如动画)可以提高性能。
  2. 分层隔离。类似 Photoshop 中的层。

从读源码的角度,layer 不影响绘制逻辑,只影响往哪里绘制,可以暂时忽略它。那我们就不找麻烦了,通过没有 layer 的情况来研究 DrawModifier 的使用。显然,核心方法是 drawContainedDrawModifiers(canvas):

// LayoutNodeWrapper
private fun drawContainedDrawModifiers(canvas: Canvas) {
  // 别忘了 entities 的链表数组
  // 那么 head 就是 DrawEntity 这个链表的第一个元素,也是最后添加的元素(别忘了是头插法添加的)
  val head = entities.head(EntityList.DrawEntityType)
  if (head == null) {
    // 没有设置任何 DrawEntity
    performDraw(canvas)
  } else {
    // 从最后一个设置的 DrawEntity 开始绘制
    head.draw(canvas)
  }
}

==先看没有 DrawModifier 的情况:==

// LayoutNodeWrapper
open fun performDraw(canvas: Canvas) {
  wrapped?.draw(canvas)
}

很直接。wrapped 是啥?研究 LayoutModifier 的时候也说过了。当具体实现是 ModifiedLayoutNode 时它就是内层的 LayoutNodeWrapper。最内层肯定是 InnerPlaceable,它的 wrapped 是 null。

等等,InnerPlaceable 的 wrapped 是 null,那组件原本的内容谁来绘制? 其实每一个组件函数内部都是创建 DrawModifier 来绘制的。所以即使我们没有手动添加,它也已经有一个或多个 DrawModifier 了。由此可见,DrawModifier 是 Compose 绘制的核心,就像 View 中的 onDraw() 方法一样。

==如果有 DrawModifier 呢?==

从上面源码可以看到它取出第一个(也是最先添加的 DrawModifier)DrawEntity 调用 draw() 方法,看下源码:

// DrawEntity
fun draw(canvas: Canvas) {
  val size = size.toSize()
  // ...
  val drawScope = layoutNode.mDrawScope
  drawScope.draw(canvas, size, layoutNodeWrapper, this) {
    with(drawScope) {
      with(modifier) {
        draw() // <--------- 终于!!最终调用 DrawModifier.draw() 方法
      }
    }
  }
}

还差一个问题,这里似乎没有链式调用呀,那剩余的 DrawModifier 怎么办? 这就要求开发者需要在 DrawModifier 里手动调用 drawContent() 方法,如果不调用的话,那确实,后面的 DrawModifier 就失效了。看一下 drawContent() 的实现:

// LayoutNodeDrawScope
override fun drawContent() {
    drawIntoCanvas { canvas ->
        val drawEntity = drawEntity!!
        val nextDrawEntity = drawEntity.next
        if (nextDrawEntity != null) {
            nextDrawEntity.draw(canvas)
        } else {
            drawEntity.layoutNodeWrapper.performDraw(canvas)
        }
    }
}

很明显,继续调用链表中的下一个 DrawModifier 了吧。如果没有下一个则调用内层的 LayoutNodeWrapper 绘制, performDraw() 的源码刚刚已经看过了。

总结:

  • Compose 从最外层的 LayoutNodeWrapper 开始绘制。
  • 如果这一层没有 DrawModifier 则深入内层,直到 InnerPlaceable 结束。
  • 若中间任意一层有 DrawModifier,则调用它(这个应该是当前层中最先添加的 DrawModifier)。在 DrawModifier 内部开发者应该调用 drawContent() 触发后面的绘制。
  • drawContent() 可能触发两种调用:
    • 若链上还有 DrawModifier 则调用它。
    • 若没有则触发内层的 LayoutNodeWrapper。

小测验

Box(modifier = Modifier.requiredSize(80.dp).background(Color.Green).requiredSize(40.dp))

绿色区域有多大?

绘制区域的大小取决于 DrawModifier 所属的 LayoutNodeWrapper。处理 Modifier 时从内向外遍历,先添加的(左边)是外侧,后添加的(右边)是内侧。所以 backgraound 属于 requiredSize(40.dp) 对应的 LayoutNodeWrapper,故绿色大小为 40dp。


再看一个例子

Box(modifier = Modifier.size(40.dp).padding(8.dp).background(Color.Green))

绿色区域大小是 24dp,看起来似乎是由它左边的 Modifier 决定的?

非也!左边的 Modifier 只决定了对右侧的传入限制,background 的大小还是由右侧的测量结果决定。右侧...有东西吗?有的!就是最内层的 InnerPlaceable 呀。搞不清这个,遇到第一题的情况就不会咯。

Modifier.drawWithContent()

DrawWithContentModifierDrawModifier 最简单的实现,它等价于这么写:

// 与 Modifier.drawWithContent{} 等价
Modifier.then(object :DrawModifier{
  override fun ContentDrawScope.draw() {
    TODO("Not yet implemented")
  }
})

注意,这个 Modifier 不是在组件的基础上额外绘制其他东西,而是直接控制整个组件的绘制。 看过了原理关于这一点应该是水到渠成的,Compose 根本没有所谓的「组件的绘制」,一切绘制都是基于 Modifier 的,包括组件本身。通常在其中需要调用一下 drawContent() 方法。

PointerInputModifier

了解了布局与绘制,现在轮到事件输入了。

与 View 类比

暂且忽略键盘、鼠标等,单单是触摸也已经是很大的话题了。在传统 View 中对事件的处理大概有三个等级:

  • 最简单的是注册监听器。例如 setOnClickListener()
  • 稍微复杂一点的可以重写事件方法,调用手势检测器 GestureDetectorCompat
  • 最复杂的就是重写事件方法,完全自己处理。

在 Compose 中也有这种区分。

  • 最简单的是使用顶层的 Modifier:

    Box(modifier = Modifier.clickable {  })
    // 或可以监听多种手势的
    Box(modifier = Modifier.combinedClickable(onDoubleClick = {}, onLongClick = {}, onClick = {}))
    
  • 复杂的使用底层 Modifier,但在内部可以使用手势检测函数:

    Box(modifier = Modifier.pointerInput(Unit) {
      detectTapGestures(onTap = {}, onPress = {}, onDoubleTap = {}, onLongPress = {})
    })
    

    不难发现 detectTapGestures()Modifier.combinedClickable() 有重复。区别在于前者更底层,比如后者内部处理了 onPress 来显示触摸反馈,没有把这个事件暴露出来。

  • 更复杂的还是使用这个 Modifier,但用的是更底层的函数:

    Box(modifier = Modifier.pointerInput(Unit) {
      forEachGesture {
        awaitPointerEventScope { /*监听处理最原始的事件*/ }
      }
    })
    

工作原理

再一次回到 LayoutNode.modifier 的 setter,PointerInputModifierDrawModifier 一起在 toWrap.entities.addBeforeLayoutModifier(toWrap, mod) 中被处理,包装成 PointerInputEntity 后添加到链表中。这里就不重复分析了,忘记的去看看 「DrawModifier 工作原理 - 处理」章节。

由此可以猜到,PointerInputModifier 针对的是它右边最近的 LayoutModifier 或组件本身。如果一层中有多个 PointerInputModifier 那左边的先被调用。 知道这个结论以及原因其实就够了。如果有兴趣,我们一起再接着研究具体的调用。

这次的调用入口在 LayoutNode.hitTest(),它负责检测这次事件应该分发到哪些组件上。依然和之前一样,它只简单地调用最外层 outerLayoutNodeWrapper 的同名方法。这个方法的核心是取链表头节点(也是最左边的 PointerInputModifier 的包装)调用 LayoutNodeWrapper.hit() 方法:

// LayoutNodeWrapper
private fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> T?.hit(
  hitTestSource: HitTestSource<T, C, M>,
  pointerPosition: Offset,
  hitTestResult: HitTestResult<C>,
  isTouchEvent: Boolean,
  isInLayer: Boolean
) {
  if (this == null) {
    hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
  } else {
    hitTestResult.hit(hitTestSource.contentFrom(this), isInLayer) {
      next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
    }
  }
}

它的核心在 12 行,这次调用的其中一个参数来自 hitTestSource.contentFrom(this),这是一个接口,并且只有匿名实现类,AS 中不能直接跳转。其中一个实现在 LayoutNodeWrapper 的 1205 行左右:

val PointerInputSource =
object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
  override fun contentFrom(entity: PointerInputEntity) = entity.modifier.pointerInputFilter
  // ...
}

看看 contentFrom() 的实现,不就是返回 PointerInputEntity 包裹的那个 Modifier 实例么,也就是我们写的 Modifier.pointerInput(Unit)。为啥要那么麻烦?主要因为 PointerInputModifier 有两个实现,其中一个用于和 View 交互,另一个才是 Compose 内部使用的,这么写是为了适配两种情况。

弄清了这个参数的来源,继续追踪 hitTestResult.hit()HitTestResult.hitInMinimumTouchTarget():

// HitTestResult
fun hitInMinimumTouchTarget(
  node: T, // 这个就是 PointerInputModifier
  distanceFromEdge: Float,
  isInLayer: Boolean,
  childHitTest: () -> Unit
) {
  val startDepth = hitDepth
  hitDepth++
  ensureContainerSize()
  values[hitDepth] = node  // <--------------- 顺序把 Modifier 加入数组
  distanceFromEdgeAndInLayer[hitDepth] = DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue
  resizeToHitDepth()
  childHitTest()           // <---------------- 调用下一个节点
  hitDepth = startDepth
}

它做了两个事:

  • 顺序把 Modifier 放进一个数组
  • 调用 childHitTest lambda 表达式。它从 LayoutNodeWrapper.hit() 传递过来,具体代码是 { next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)}

可以想象,整体来讲这部分是把链表的元素一个个提取出 Modifier 放进数组保存。所以数组中 Modifier 的顺序与我们写的代码顺序一样,从左到右,从先应用的到后应用的。

其实在放进数组之前 Compose 做了判断,只有本次事件应该触达的那些组件的 PointerInputModifer 会被放进去。那么这个数组就是需要处理本次事件的 Modifier,后面依次调用下就行了。不过这个调用不再是 LayoutNode 的工作,这里也不深究了。我们目前研究的核心是搞清楚不同 Modifier 的处理顺序与相互影响,不能无限展开。

ParentDataModifier

ParentDataModifier 用于辅助测量布局。从名字可以略窥一二,这类 Modifier 主要是给父组件看的,而不是所属的那个组件。weight 就用到了此接口。

在传统 View 系统中也采用了相同的方案。所有 layout_ 开头的布局属性都由父 View 处理。

想象一下用 LayoutModifier 来实现 weight 功能。Modifier 内部只能获取 1. 父组件给自己的限制;2. 原始测量结果。但是 weight 还需要综合其他子组件的权重才能计算自己的大小,显然,实现不了。这就是 ParentDataModifier 的用武之地。

自定义使用

由于 ParentDataModifier 需要父组件配合才能生效,所以是个完整可用的 ParentDataModifier 需要编写至少两部分代码:

  • 一个 Modifier 扩展函数,便于用户创建我们的 ParentDataModifier。
  • 自定义组件,并解析我们的 ParentDataModifier。

其中自定义组件是另一个大话题,这里仅作演示,不深入。假如要自己实现一个 weight,可以这么写:

// 扩展函数,方便用户创建 ParentDataModifier
fun Modifier.myWeight(weight: Float) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return weight
    }
})

// 自定义布局,省略了真正的布局代码
@Composable
fun MyLayout(modifier: Modifier, content: @Composable () -> Unit) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        measurables.forEach {
            val data = it.parentData as? Float
            // 利用这个 data 去测量布局
        }
        layout(100, 100) { /*...*/ }
    }
}

在自定义布局中,measurables 参数是所有子组件对应的 Measurable,通过它的 parentData 属性可以拿到附加的 ParentDataModifier 提供的值(如果有)。这个值可能是任何类型,根据需要判断与强转。

如果多次附加 ParentDataModifier 呢? 仔细看看 ParentDataModifier 的实现类,方法接受了一个参数,这就是右边的 ParentDataModifier 所提供的数据,它的返回值会提供给左边的 ParentDataModifier 或作用到组件上。如果需要的话可以融合多个值,不过更多情况下只需要最新值就行了。

那如果需要多种数据咋办? 这种需要就得写一个类承载多个数据然后做融合了。比如这样:

class MyLayoutData(var weight: Float = 0f, var expended: Boolean = false)

fun Modifier.myWeight(weight: Float) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any =
        ((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.weight = weight }
})

fun Modifier.expended(expended: Boolean) = then(object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any =
        ((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.expended = expended }
})

这样写有个巨大问题:API 污染。 现在 Modifier.expended() 在任何地方都可以调用,但显然只有我们自己的 MyLayout 组件才能识别。不仅开发者会受到 IDE 提示的打扰,也有可能影响其他组件的工作。为此,应该创建一个作用域接口来限制这些方法的使用。

@LayoutScopeMarker // 禁止在间接作用域里调用(Kotlin 语法允许在间接作用域里调用)
@Immutable
object MyLayoutScope {
    fun Modifier.myWeight(weight: Float) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any =
            ((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.weight = weight }
    })

    fun Modifier.expended(expended: Boolean) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any =
            ((parentData as? MyLayoutData) ?: MyLayoutData()).also { it.expended = expended }
    })
}

虽然创建接口,再用 object 创建一个单例是标准写法(看看 Row 怎么写的),但是太麻烦了,作为例子我们直接创建了一个单例。然后为我们的自定义 Layout 提供作用域:

@Composable
fun MyLayout(modifier: Modifier, content: @Composable MyLayoutScope.() -> Unit) {
    Layout(content = { MyLayoutScope.content() }, modifier = modifier) { measurables, constraints ->
        measurables.forEach {
            val data = it.parentData as? Float
            // 利用这个 data 去测量布局
        }
        layout(100, 100) { /*...*/ }
    }
}

工作原理

ParentDataModifier 的处理与存储方式与 PointerInputModifier, DrawModifier 都一样,最后被存在一个链表中。

烦了,不说了,自己去看之前的分析 (メ`[]´)/

直接看怎么被调用的。不是我们自己调用的吗?那就跟踪 IntrinsicMeasurable.parentData 的实现,显然,应该看 LayoutNodeWrapper 这一个:

// LayoutNodeWrapper
override val parentData: Any?
    get() = entities.head(EntityList.ParentDataEntityType).parentData

private val SimpleEntity<ParentDataModifier>?.parentData: Any?
    get() = if (this == null) {
        wrapped?.parentData
    } else {
        with(modifier) {
            /**
             * ParentData provided through the parentData node will override the data provided
             * through a modifier.
             */
            modifyParentData(next.parentData)
        }
    }

注意,这次拿到的 headSimpleEntity,它没有 parentData 属性,而是 LayoutNodeWrapper 中给它添加了一个扩展属性。思路依然和之前类似:如果当前层(LayoutNodeWrapper)没有关联的包装有 ParentDataModifierSimpleEntity,就进入内层,否则先递归执行右边的 ParentDataModifier 或进入内层,拿到结果作为参数再执行这一个。

虽然储存结构上 ParentDataModifier 也是绑定在它右边最近的 LayoutModifier 或组件本身上,但它不直接操作组件。所以到底和谁绑定不影响父组件读取它,自然也不影响显示效果。

从技术上,组件自己的 LayoutModifier 也可以读取到 parentData 的数据,但没有什么用。

SemanticsModifier

SemanticsModifier 用于构建 Semantics Tree,这是啥?

我们知道经过组合,Compose 会生成 UI 树,其中每个组件是一个节点。如果把这个树精简一下,删除/合并一些节点只留下有意义的东西,得到的就是 Semantics Tree 语义树。所谓「有意义」指可以供用户查看/操作的独立组件,例如图片,而一个单纯用于布局 Row 在这个语境下就没有意义。

Semantics Tree 主要用于:1. 无障碍功能。2. 开发者测试(用于查找组件,类似与 View 的 id)。

使用

一个简单的组件:

Box(modifier = Modifier.width(80.dp).background(Color.Green))

在 Talk Back 开启的情况下用户触摸这个组件系统不会有任何反映。因为默认情况下 Compose 认为它是无意义的。但若这个组件偏偏有意义怎么办?比如我就是想用一个红色的方框代表已禁用(虽然很难看),现在视觉障碍用户完全不知道这一含义。此时可以使用 semantics 来设置一个 contentDescription:

Box(modifier = Modifier.width(80.dp).background(Color.Green).semantics {
  contentDescription = "选项已禁用"
})

这么设置之后用户再触摸这个区域,系统就会读出这句描述。

插一句题外话,大陆对残障人士太不友好了,断掉的盲道,45度的轮椅坡,拥挤的公交车...当然,我们有很多客观理由,比如还是发展中国家,人口太多等等等。

但作为一名开发者,尤其当你是一名没有被资本绑架的个人开发者时,希望能够多考虑一下无障碍支持,而不是写一个没意义的字符串来抑制 warn。相比传统行业,我相信技术圈还是有不少人,依然心怀理想。

再看一个例子:

Button(onClick = { /*TODO*/ }) {
  Text(text = "打开新窗口")
}

如果点击 Text 是读文本呢还是读按钮呢?其实无论点 Text 还是点按钮的其他部分,系统都会一起读。因为在 Semantics Tree 中这两个组件被合并为一个节点了。大部分需要合并的场景 Compose 已经做了自动合并,个别没有识别的场景可以通过 Modifier.semantics(mergeDescendants = true) 指明需要合并子组件,并且防止自己被父组件合并。

还有一个 Modifier.clearAndSetSemantics{ } 函数,正如起名,它强制清除内部组件的无障碍信息。

工作原理

SemanticsModifier 又又又是和 DrawModifier 一样的储存与处理方式。那个链表数组,第四遍了,大家应该能记得了吧。

现在只看具体调用。其实具体调用也不用看了,SemanticsModifierPointerInputModifier 共用一套机制,都需要根据触摸点去找组件。入口方法在 LayoutNode.hitTestSemantics()

OnRemeasuredModifier

OnRemeasuredModifier 的调用时机相当于 View 系统里的 onMeasure 写法。

使用

在传统的 View 中我们覆盖 View.onMeasure() 一般有两种需求:

  1. 获取到 View 的尺寸。
  2. 干预测量。

在 Compose 中,这两种用法被分开了,代码更清晰。干预测量应该用前面说的 LayoutModifier

获取组件尺寸:

 Box(modifier = Modifier.onSizeChanged { intSize ->  }

它创建的是 OnSizeChangedModifier,做了一些优化,只有在尺寸改变的时候才调用回调,省得我们自己额外判断了。

其实这种 Modifier 更多情况下是与其他的一起使用。例如 Modifier.scrollable() 内部也实现了这个接口,因为它需要获取子组件的大小才好安排滚动。

工作原理

OnRemeasuredModifier 的处理与前面 DrarModifier 之类有一点点不同,不过还是在 LayoutNode.modifier 的 setter 里:

// LayoutNode
// 省略部分代码
internal val innerLayoutNodeWrapper = InnerPlaceable(this)
override var modifier: Modifier = Modifier
    set(value) {
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod: Modifier.Element, toWrap:LayoutNodeWrapper ->
            // mod 是遍历到的那一个 Modifier,toWrap 是上一轮应用的结果(或初始值)
            toWrap.entities.addBeforeLayoutModifier(toWrap, mod)
            // ... 处理 LayoutModifier
            wrapper.entities.addAfterLayoutModifier(wrapper, mod)  // <-----------!!!                                          
        }
        outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
        layoutDelegate.outerWrapper = outerWrapper
    }

OnRemeasuredModifier, OnPlacedModifier, LookaheadOnPlacedModifier 这三类在 addAfterLayoutModifier() 中处理。这和 AddBefore 有什么区别?

回顾一下,这俩函数是把 Modifier 添加到链表中,这个链表保存在当前的 ModifiedLayoutNode (LayoutModifier 包装类) 中。AddBefore 与 AddAfter 只有在一个对象同时实现了 LayoutModifier 和其他类 Modifier 时才有区别。对于前者的处理,其他类的 Modifier 会附加到右边最近的 ModifiedLayoutNode 中;对于后者的处理会附加到自己的 ModifiedLayoutNode 上。

在新版的代码中有一些变动。LayoutModifier 不再包装后存到某层(ModifiedLayoutNode)的链表数组中,而是直接保存到 LayoutNode.nodes 里,它是一个 NodeChain,是 Modifier.Node 的双链表,不区分层。但是分层关系还是有的。

Modifier.Node 是新的 Modifier 的包装,并添加一些附加信息,比如属于哪个 NodeCoordinatorNodeCoordinator 相当于原来的 ModifiedLayoutNode,是 LayoutModifier 专属包装。

至于具体的调用,我们利用 IDE 轻松找到来源于 LayoutNodeWrapper.onMeasured(),继续寻找,可以找到两个上游调用:

  • InnerPlaceable.onMeasured() 这是测量组件本身尺寸的
  • ModifiedLayoutNode.onMeasured() 这是 LayoutModifier 的测量

逻辑比较清晰:测量完成就调用属于这一层的 OnRemeasuredModifier 回调,如果有多个 LayoutModifier 就会多次触发回调。

比如这么写:

Modifier.padding(8.dp).onSizeChanged {  }.padding(10.dp)

onSizeChanged 在 10dp 的那个 padding 测量完成之后调用,拿到的也是它的尺寸。

OnPlacedModifier

OnPlacedModifierOnRemeasuredModifier 有点类似,都是添加一个回调,只是这一个是「被摆放」回调。它们俩触发时机分别像 View.onLayout()View.onMeasure()

使用

View.onLayout()View.onMeasure() 一样有两个作用:

  1. 能够获得自己尺寸与位置。
  2. 需要布局子 View(调用子 View 的 layout() 方法)。

Compose 中这两功能也被分开了,OnPlacedModifier 只用于获得自身位置尺寸的需求

既然类似,这个有和 OnRemeasuredModifier 有啥区别?

OnPlacedModifier OnRemeasuredModifier
调用时机 布局流程结束 测量完毕,依然在布局流程中
回调数据 尺寸、位置等完整布局信息 尺寸信息

工作原理

OnPlacedModifier 的处理与存储与 OnRemeasuredModifier 一模一样,不多说了。

利用 IDE 寻找调用方,也是找到了两个:

  • LayoutNode.onLayoutComplete()。从源码明显看出是 InnerPlaceable 布局完成后触发的。

    // LayoutNode
    override fun onLayoutComplete() {
      // innerLayoutNodeWrapper 就是 InnerPlaceable
      innerLayoutNodeWrapper.entities.forEach(EntityList.OnPlacedEntityType) {
        it.modifier.onPlaced(innerLayoutNodeWrapper)
      }
    }
    
  • LayoutNodeWrapper.onPlaced()。显然这一个就是 LayoutNodeModifier 所生成的包装类触发的。内部遍历这一层所有的 OnPlacedModifier 依次调用它们。

事实上 OnPlacedModifier 回调中得到的 LayoutCoordinates 就是 ModifiedLayoutNode,它实现了接口。

LookaheadOnPlacedModifier

这是一个 internel 类,虽然名字相似但没有实现 OnPlacedModifier

Lookahead 就是往前看看。用编译器举个例子,扫描下面字符串时:

for (int i = 0; i<10; i++)
  ^

当扫描到 for 单词的末尾,编译器无法判定这是一个循环开始普通变量(比如 forecast),需要先往下看看,才能确定已经扫描完的这部分是什么。

Compsoe 借用了这个概念来满足 LookaheadLayout 的需要。LookaheadLayout 会进行两次测量,第一次是预先测量(与摆放)把结果存起来,根据预先测量的结果按需添加 Modifier 再进行真正的测量。主要用于过渡动画,这里不详细研究了。

回到 LookaheadOnPlacedModifier,它也是一个「被摆放」的回调,区别是除了真正的数据外,还额外提供了预先测量的数据。只能在 LookaheadLayout 的子组件身上使用,也是 Modifier.onPlaced{} 函数,但是 lambda 表达式接受两个参数。

LookaheadOnPlacedModifier 的调用时机与普通的 OnPlacedModifier 一样,不受「预先测量」这种行为影响。只不过被调用的时候把保存的预先测量的结果也传过去了。

OnGloballyPositionedModifier

又是一个回调 Modifier,并且回调的参数与 OnPlacedModifier 一样?!

那是因为 OnGloballyPositionedModifier 也是回调尺寸与位置,回调的是它所属的那一层(ModifierLayout / ModifiedLayoutNode)所负责的区域位置/尺寸。

这...听起来似乎和 OnPlacedModifier 没区别啊?区别在于回调时机:

  • OnPlacedModifier 回调时机是所属的那一层测量完成之后,外层摆放它时。这就意味着它内层还没有被摆放,可以做一些处理来影响内层摆放。 比如这样:

    var offsetX by remember { mutableStateOf(0) }
    var offsetY by remember { mutableStateOf(0) }
    Modifier
      .onPlaced {
        offsetX = (it.positionInParent().x / 2).toInt()
        offsetY = (it.positionInParent().y / 2).toInt()
      }
      .offset {
        IntOffset(offsetX, offsetY)
      }
    

    offset 的回调发生在 onPlaced 之后,可以受到影响。

  • OnGloballyPositionedModifier 回调对于位置来讲,只有相对于 Windows 位置改变的时候才触发。 比如聊天列表界面是可以滑动的,每一项都有一个头像。上下滑动时头像的位置相对于父组件没有移动,所以 OnPlacedModifier 不触发。但是相对于 Windows 是改的,所以 OnGloballyPositionedModifier 触发。这就是「Globally」的含义

那有没有什么情况可以触发 OnPlacedModifier 但不触发 OnGloballyPositionedModifier 呢?

理论上有的。比如父组件往左移动 100px,子组件往右移动 100px,此时子组件相对 Windows 没有移动。但事实上,OnGloballyPositionedModifier 并没有进行前后位置比较,而是只要它位置变了,就回调。 Compose 故意这么实现的。

显然,OnGloballyPositionedModifier 回调的次数更多,所以尽量使用轻量级的 OnPlacedModifier

ModifierLocal

说到 Local,应该可以联想到另外两个:CompositionLocal, ThreadLocal, 他俩表示这个数据在某个范围内可共享,ModifierLocal 一样。

ModifierLocal 在 SDK / 公共库中比较常用,App 开发用的不多。

使用

与 CompositionLocal 类似,ModifierLocal 的使用分为三步:

  1. 创建一个对象。
  2. 提供值。
  3. 取用值。
// 创建一个 ModifierLocal 对象
val sharedData = modifierLocalOf { "NA" }
Modifier
  // 提供值
  .modifierLocalProvider(sharedData){ "New Dara" }
  // 取值
  .modifierLocalConsumer { 
    val data = sharedData.current
  }
}

这...看起来它们在独立的 Modifier 中,怎么才能给现有的 Modifier 提供值呢?这就麻烦了,我们得舍弃便捷函数,手动创建 Modifier 并让它实现 ModifierLocalProviderModifierLocalConsumer 接口。

例如 Modifier.layout{} 如果想读取共享数据,得这么写:

Modifier.then(object : LayoutModifier, ModifierLocalConsumer {
  lateinit var data: String
  override fun MeasureScope.measure(
    measurable: Measurable,
    constraints: Constraints
  ): MeasureResult {
    // 这里可以放心使用 data
    val p = measurable.measure(constraints)
    return layout(p.width, p.height) {
      p.placeRelative(0, 0)
    }
  }

  override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
    with(scope) {
      data = sharedData.current // 接收共享数据
    }
  }
})

onModifierLocalsUpdated() 在测量之前被调用,所以 LayoutModifier 中可以放心地使用共享数据。

也可以同时实现 Prodiver 与 Consumer 接口,用于接收上游数据,处理之后再继续往下游传递。如果同时实现,Compose 可以保证 onModifierLocalsUpdated() 先被调用,然后才是 Provider 接口的提供数据的方法。