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

本节源码参照 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 有啥区别?

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

工作原理

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 接口的提供数据的方法。

Last modification:November 21, 2022