Jetpack Compose 之 Modifier(上)

modifier:Modifier = Modifier 的含义

kotlin 复制代码
companion object : Modifier {
    override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
    override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
    override fun any(predicate: (Element) -> Boolean): Boolean = false
    override fun all(predicate: (Element) -> Boolean): Boolean = true
    override infix fun then(other: Modifier): Modifier = other
    override fun toString() = "Modifier"
}

companion object 会生成一个单例对象出来,不过,就生成单例对象这个操作而言,仅仅靠Object已经可以实现了。而 companion 又给这个单例对象提供了一个额外的特性,那就是你在哪个类里边或者接口声明了这个单例对象的,那么就可以用这个类或者接口的名字来代表这个单例对象,比如上边的Modifier是在Modifier这个接口中申明的:

所以我们就可以用Modifier这个名字来代表单例对象。比如在 Jetpack Compose 中,经常会在 Composable 函数中看到如下参数声明:

kotlin 复制代码
modifier: Modifier = Modifier

这样写也是等价的:

kotlin 复制代码
val modifier: Modifier = Modifier.Companion

这句话的含义和作用如下:

  • 参数名称与类型

    • modifier:这是参数名称,通常用来允许调用者向该 Composable 提供一系列对布局、绘制或交互效果的修饰(Modifier)。
    • Modifier:这是 Compose 提供的修饰符类型。它是一个不可变的、链式的修饰集合,用于描述如何修改组件的布局、绘制和行为,例如添加边距、背景色、点击事件、填满父容器等。
  • 默认值 Modifier

    • 当我们写 modifier: Modifier = Modifier 时,表示如果调用该 Composable 时没有传入任何修饰,则默认使用一个空的 Modifier 实例(也就是没有任何修饰)。
    • 这种写法使得该参数是可选的,我们可以选择忽略它,也可以在需要时链式组合多个 Modifier。例如:modifier = Modifier.padding(16.dp).background(Color.Red)
  • 作用与优势

    • 可复用与扩展性:在自定义组件时,加入 modifier 参数能够让调用者在外部对组件进行额外的修饰,而不必修改组件内部的布局或行为。
    • 默认空修饰符:使用默认值 Modifier 确保了当没有额外修饰时,组件依然能够正常渲染,不会因缺少修饰而影响布局。

示例

Compose 官方建议,如果一个自定义的组件中有 Modifier 参数,且它有默认值,那么建议将Modifier写在第一个,因为 Kotlin 中第一个参数可以不用写参数名。

假设有一个简单的自定义组件,它接受一个 Modifier 参数:

kotlin 复制代码
@Composable
fun Greeting(
    modifier: Modifier = Modifier, // 默认空修饰符
    name: String,
) {
    Text(
        text = "Hello, $name!",
        modifier = modifier // 将传入的 Modifier 应用于 Text
    )
}

调用时可以选择传递修饰符:

kotlin 复制代码
@Composable
fun GreetingScreen() {
    Greeting(
        modifier = Modifier
        .padding(16.dp)
        .background(Color.Yellow),
        name = "Compose"
    )
}

或者不传递,则使用默认值:

kotlin 复制代码
@Composable
fun DefaultGreetingScreen() {
    Greeting(
        Modifier
        .padding(16.dp)
        .background(Color.Yellow),
        name = "Compose")
}

这种设计使得组件在默认情况下也能正常工作,同时又允许外部调用者轻松扩展组件的视觉外观和行为。

总结

  • modifier: Modifier = Modifier 声明了一个名为 modifier 的参数,其类型为 Modifier。
  • 默认值 Modifier 表示一个空修饰符链,当没有传入其他修饰时,组件不会受到任何附加修饰。
  • 这种写法让组件更加灵活和可复用,便于在不同场景下对组件进行外部定制。

Modifier

then() CombinedModifier 和 Modifier.Element

1. then() 方法

then() 方法是 Modifier 接口的一部分,用于将两个 Modifier 合并起来。它可以用来顺序地连接多个 Modifier 实例,从而按顺序应用这些修饰效果,内部实际的实现是由CombinedModifier实现的。

kotlin 复制代码
infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)
语法:
kotlin 复制代码
fun Modifier.then(other: Modifier): Modifier
用法:

一般的,then() 方法返回一个新的 Modifier,该 Modifier 会应用当前的修饰符(即调用者)和 other 修饰符。顺序非常重要,other 修饰符会在当前修饰符之后应用。不过,如果你这样写的话:Modifier.background(Color.Blue).then(Modifier), 根据源码,返回的就是 Modifier.background(Color.Blue), 后边的then(Modifier)会被扔掉。

示例:
kotlin 复制代码
val combinedModifier = Modifier.padding(16.dp).then(Modifier.background(Color.Red))

在这个例子中,combinedModifier 会首先应用 padding,然后再应用背景色 Red。效果是一个有内边距且背景为红色的元素。

解释:
  • 我们可以通过链式调用多个 Modifier,并且每个 Modifier 的效果会按照调用的顺序应用。
  • then() 方法通常用于合并多个修饰符,但也可以用于有条件地附加修饰符。
CombinedModifier

CombinedModifier 是 Jetpack Compose 内部用于合并多个 Modifier 的一种机制。在实际开发中,我们一般不会直接使用 CombinedModifier,但它在 Compose 的实现中起到了组合多个修饰符的作用。

CombinedModifierModifier 的一个实现类,它通过将多个 Modifier 实例依次合并 成一个高效的组合修饰符来减少不必要的性能开销。这种优化是 Compose 框架为提高性能而进行的内部优化,通常开发者不需要显式操作它。CombinedModifier 用于优化多个 Modifier 的合并,使得组合后的修饰符能以最低的性能代价被执行。

kotlin 复制代码
class CombinedModifier(
    private val outer: Modifier, // then 函数的调用者
    private val inner: Modifier // then 函数的参数
) : Modifier {
    // 对其中一个调用,然后把其中一个调用的结果作为参数传递给另外一个调用的函数的参数里边去
    override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
        inner.foldIn(outer.foldIn(initial, operation), operation)

    override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
        outer.foldOut(inner.foldOut(initial, operation), operation)

    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)

    override fun equals(other: Any?): Boolean =
        other is CombinedModifier && outer == other.outer && inner == other.inner

    override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()

    override fun toString() = "[" + foldIn("") { acc, element ->
        if (acc.isEmpty()) element.toString() else "$acc, $element"
    } + "]"
}

实例演示调用效果,对于以下的代码:

kotlin 复制代码
modifier1.then(modifier2)

使用foldIn的时候,会先调用modifier1,然后再调用modifier2, modifier1.then(modifier2) 的执行顺序:

  1. 第一个步骤modifier1 调用其 foldIn 方法,处理 modifier1 的所有元素。
  2. 第二个步骤modifier1 处理完后,将结果传递给 modifier2inner),然后在 modifier2 上执行 foldIn

所以,整体上,modifier1 会先被处理,然后再处理 modifier2,这是因为 then() 方法组合的是 modifier1modifier2,并通过 foldIn 方法按照这个顺序进行递归调用。

下边的两行代码执行效果是一样的:

kotlin 复制代码
CombinedModifier(Modifier.background(Color.Red), Modifier.padding(8.dp))
Modifier.background(Color.Blue).then(Modifier.padding(8.dp))

还有下边这种写法效果也是相同的,但是可以看到使用CombinedModifier 的时候,明显代码要复杂一些。

kotlin 复制代码
Modifier.background(Color.Blue).then(Modifier.padding(8.dp))
  .then(Modifier.padding(8.dp))
  .then(Modifier.size(80.dp))
CombinedModifier(
  CombinedModifier(Modifier.background(Color.Blue), Modifier.padding(8.dp)),
  Modifier.size(80.dp))

2. Modifier.Element

Modifier.ElementModifier 接口的一个子接口,表示 Modifier 的基本构件。Element 用于标记那些直接执行修饰行为的元素,通常是对布局、点击事件、动画等操作的定义。在Modifier中,除了Modifier的半生对象和 CombinedModifier,其余所有的子接口或者子类,全部都是直接或间接的继承或实现了 Modifier.Element,这样就让实现类居右了遍历叶子节点的能力。而这些能力最终又会被CombinedModifier所利用。

kotlin 复制代码
interface Element : Modifier {
  // foldIn 方法用于从外向内(或说从最外层到当前元素)的顺序地"折叠"(也就是聚合)Modifier 链。
  // 在这里,对于一个单一的 Modifier.Element,
  // 它就简单地将初始值 initial 和当前元素 this 传入操作函数 operation 进行处理,并返回计算后的结果。
  override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
    operation(initial, this)
  // foldOut 方法与 foldIn 类似,不过其遍历的顺序是相反的------它从当前元素开始,向外层进行折叠操作。
  // 对于单一的 Modifier.Element,直接把当前元素 this 和初始值 initial 传递给操作函数。
  override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
    operation(this, initial)
  // any 方法用于判断 Modifier 链中是否存在至少一个元素满足给定的条件。
  // 对于单一的元素,它只需对当前元素应用 predicate(谓词函数),返回判断结果。
  override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)

  // all 方法则用于判断 Modifier 链中所有元素是否都满足某个条件。
  // 对于单一的 Modifier.Element,直接判断当前元素是否满足即可。
  override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
}
  • 聚合操作(foldIn/foldOut)
    foldIn先加入的Modifier先应用,而foldOut则相反。 这两个函数允许我们以不同的顺序(由外向内或由内向外)遍历 Modifier 链,并结合所有元素的信息生成一个最终的聚合值。比如,你可以利用它们来组合所有修饰符的视觉效果、查找特定类型的修饰符或者计算累计的修饰量。

  • 条件检查(any/all)

    这两个函数提供了一种机制,可以在 Modifier 链上进行条件判断。any 用于检查链中是否存在满足条件的修饰符,而 all 则用于确认链中所有修饰符都满足条件。

语法:
kotlin 复制代码
interface Modifier.Element : Modifier

Element 接口是一个标记接口,通常我们不会直接实现这个接口。它的目的是提供给 Modifier 的子类一个统一的标记,以便 Compose 系统能够区分哪些修饰符是属于 Element 类型的,哪些是需要组合或其他操作的。

示例:
kotlin 复制代码
val paddingModifier = Modifier.padding(16.dp) // 这是一个 Modifier.Element 类型

Modifier.padding(16.dp) 创建了一个 Modifier.Element 实例,它在 Compose UI 树中表示具体的布局操作。padding 作为修饰符会直接影响其应用的视图。

总结

  • then() :用于将多个 Modifier 实例按顺序合并,并返回一个新的 Modifier,即允许你链接多个修饰符。
  • CombinedModifier :Compose 内部机制,用于高效地组合多个 Modifier 实例,避免性能开销。通常不需要直接操作它。
  • Modifier.Element :是 Modifier 接口的子接口,表示直接执行修饰操作的构件。通常不会显式地实现它,而是依赖于 Compose 提供的修饰符。

Modifier.composed()和ComposedModifier

Modifier 一般是轻量级且不具备自身状态的,但有时我们希望自定义的修饰符能够访问组合(composition)的能力,比如利用 remember 保存状态、读取 CompositionLocal 或执行其他需要组合环境的操作。为此,引入了两个相关概念:Modifier.composed()ComposedModifier,它们分别起到下面的作用:

  • 作用与目的
    Modifier.composed() 是一个扩展函数,用于创建"组合型"的修饰符。通过这个扩展,我们可以在修饰符中编写带有组合(composable)语义的逻辑。这意味着我们可以在其中使用 Compose 的 API,如 rememberSideEffectCompositionLocal 等,从而赋予修饰符状态或其他组合特性。

  • 使用场景

    在编写一个修饰符时,如果仅仅是对传入 View 或 Composable 的绘制、布局进行简单改变,那么通常直接使用简单修饰符就足够了;

    但如果需要:

    • 在修饰符内部保存状态;
    • 执行依赖于组合生命周期的操作;
    • 根据环境动态生成修饰效果;
      那么就可以使用 Modifier.composed { ... } 来包装这些逻辑。
  • 示例代码

    kotlin 复制代码
    fun Modifier.myCustomModifier(): Modifier = composed {
        // 这里可以访问组合内的功能,例如记住状态
        val someState = remember { mutableStateOf(0) }
        // 根据状态或其他逻辑返回最终的修饰符
        this.then(Modifier.background(if (someState.value > 0) Color.Green else Color.Red))
    }

    在这个例子中,composed 为我们提供了一个组合上下文,在其中我们可以调用 remember 等 API。

  • 注意事项

    使用 composed() 会略微增加内部开销,因为它引入了组合的过程,所以只在需要组合能力时使用,而对于简单的修饰符链条,推荐直接使用普通修饰符。


ComposedModifier

  • 作用与内部实现
    ComposedModifier 是 Compose 内部用于实现 Modifier.composed() 生成的修饰符的一种具体实现类(或说机制)。当你调用 Modifier.composed { ... } 时,底层会把我们传入的组合逻辑封装进一个 ComposedModifier(或类似实现)的实例中。
kotlin 复制代码
private open class ComposedModifier(
    inspectorInfo: InspectorInfo.() -> Unit,
    val factory: @Composable Modifier.() -> Modifier
) : Modifier.Element, InspectorValueInfo(inspectorInfo)

ComposedModifier 的工作流程,我们通过一个实例来进行分析:

kotlin 复制代码
// (1)
Box(Modifier.composed { Modifier.padding(10.dp) })

// (2)
fun Box(modifier: Modifier) {
    Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
}

// (3)
@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    // 省略无关代码
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

// (4)
@PublishedApi
internal fun materializerOf(
    modifier: Modifier
): @Composable SkippableUpdater<ComposeUiNode>.() -> Unit = {
    val materialized = currentComposer.materialize(modifier)
    update {
        set(materialized, ComposeUiNode.SetModifier)
    }
}

// (5)
fun Composer.materialize(modifier: Modifier): Modifier {
    // 省略无关代码。。。。。。

    val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
        acc.then(
            if (element is ComposedModifier) {
                @kotlin.Suppress("UNCHECKED_CAST")
                val factory = element.factory as Modifier.(Composer, Int) -> Modifier
                val composedMod = factory(Modifier, this, 0)
                materialize(composedMod)
            } else {
                // 省略无关代码。。。。。。
                newElement
            }
        )
    }
    endReplaceableGroup()
    return result
}

按照顺序执行,最终进入了(5),把整个链条上的ComposedModifier都检查一遍,如果是,就把ComposedModifier 都替换成它的工厂函数调用之后所返回的Modifier, 替换原来的ComposedModifier,对于非ComposedModifier,都保持不变。 我们也同时知道了ComposedModifier就是在组合的过程中调用的。从源码流程中我们知道:Box(Modifier.composed { Modifier.padding(10.dp) })这个代码执行的时候,Modifier.composed 内部的lambda表达式并不是在 Modifier.composed 被当做一个参数传递进来的时候就开始执行。而是在后续的某个Composation 的过程中被创建了,这个有什么区别呢?用处在哪里呢?

kotlin 复制代码
@Composable
fun ModifierComposedDemo() {
  // 使用 remember 记住一个可变的 padding 大小,初始值为 8.dp
  var padding by remember { mutableStateOf(8.dp) }

  val modifier1 = Modifier.composed { 
    var padding by remember { mutableStateOf(8.dp) }
    Modifier.padding(padding).clickable { padding = 0.dp }
  }

  val modifier2 = Modifier.padding(8.dp).clickable { padding = 0.dp }
  Column {
    Box(modifier = Modifier.background(Color.Blue).then(modifier1))
    Text(text = "example", modifier = Modifier.background(Color.Green).then(modifier1))

    Box(modifier = Modifier.background(Color.Blue).then(modifier2))
    Text(text = "example", modifier = Modifier.background(Color.Green).then(modifier1))
  }
}

在上边的代码中,如果 Box 和 Text 都使用 modifier2 话,会导致无论点击哪一个,两者的padding都会变为0,而使用 modifier1 则不会影响对方,因为他们各自在内部运行了一次工厂函数。

  • 功能

    • 捕获组合环境:能在修饰符中访问 Compose 的组合功能,比如状态、CompositionLocal 等。
    • 支持重组(Recomposition) :一旦组合环境中的数据发生变化,自定义修饰符也能根据需要自动重组。
    • 内部缓存 :为了避免不必要的重建,ComposedModifier 还会对内部的计算结果进行缓存或记忆,从而提高性能。
  • 用户角度

    对于大多数开发者来说,通常不需要直接与 ComposedModifier 类型打交道,而只需要调用 Modifier.composed { ... } 即可;
    ComposedModifier 更多的是 Compose 框架内部用来管理和调度组合型修饰符的一种实现细节。


在项目中的使用

一般不会在下边这种情况下使用:

kotlin 复制代码
Column {
  var padding1 by remember { mutableStateOf(8.dp) }
  val paddingModifier = Modifier.padding(padding1).clickable { padding1 = 0.dp }
  var padding2 by remember { mutableStateOf(8.dp) }
  val padding2Modifier = Modifier.padding(padding2).clickable { padding2 = 0.dp }

  Box(Modifier.background(Color.Blue) then paddingModifier)
  Text("example", Modifier.background(Color.Green) then padding2Modifier)
}

一般我们会在自定义 Modifier 的时候使用,当我们创建的 Modifier ,我们需要在其内部添加一些内部状态,这个时候,我们要包裹一个 composed 函数来提供一个 Composable 的上下文环境,从而可以让我们调用 remember ,这样就可以让我们返回的 Modifier 有状态了,同时由于工产函数是延迟执行的,并且每次调用都会执行,因此另一个特点就是每一处的调用之间的状态是不会相互影响的。这就是 compose 这个函数的本质。

kotlin 复制代码
fun Modifier.paddingJumpModifier() = composed {
  var padding by remember { mutableStateOf(8.dp) }
  Modifier
    .padding(padding)
    .clickable { padding = 0.dp }
}

另外因为 composed 可以提供一个 Composable 的上下文环境,所以我们可以很方便在其内部使用 LaunchedEffect 来利用协程做更多的操作。

kotlin 复制代码
fun Modifier.coroutineModifier() = composed {
  LaunchedEffect(key1 =, block =)
    ...Modifier
}

如果想要在Modifier中获取某个 CompositionLocal 的读取,也可以利用 composed

kotlin 复制代码
// CompositionLocal.current get()
fun Modifier.localModifier() = composed {
  LocalContext.current
  Modifier
}

总结

  • Modifier.composed()
    是一个公开 API,允许我们在修饰符中使用组合功能;它开启了一个组合环境,使得我们可以在修饰符逻辑里调用 remember、读取 CompositionLocal 以及其他组合相关函数,从而编写更灵活、动态的修饰符。
  • ComposedModifier
    则是这种"组合型"修饰符的内部实现,它封装了由 Modifier.composed() 构建的逻辑,并保证这些逻辑能够正确参与 Compose 的重组与状态管理过程。

因此,当我们需要在自定义修饰符中使用 Composable的环境时,用 Modifier.composed() 封装我们的逻辑;底层会由 ComposedModifier 接管,从而保证这些修饰符在组合过程中能够正确工作、响应状态变化。


Modifier.layout()

1. Modifier.layout() 介绍

  • Modifier.layout() 用来修改组件的尺寸和位置偏移。
  • 它允许我们传入一个 Lambda,在这个 Lambda 中就能获取到当前组件的测量器(Measurable)和约束(Constraints),并通过自定义测量逻辑返回合适的布局结果。
1.1 工作原理
  • 在调用 Modifier.layout { measurable, constraints -> ... } 时,内部会创建一个实现了 LayoutModifier 接口的对象。此处传入的 constraints 确实代表了外层组件(父布局)对子组件的约束条件,比如最大宽度、最大高度以及最小尺寸等。我们可以在这个基础上进一步调整或修改,比如增加一些额外的空间(例如 padding),调用Modifier.layout 时候,相当于在外部组件和内部组件之间又加了一层。例如,假设我们希望在组件外部添加 padding,那么在测量阶段我们可以调整 constraints 或者在布局阶段对 Placeable 进行偏移,从而达到类似 padding 的效果。:

    kotlin 复制代码
    Box( modifier = Modifier.background(Color.Yellow)) {
       Text(
           text = "renwuxian",
           modifier = Modifier.layout { measurable, constraints ->
    
               // 1. 定义"padding"大小(单位是像素,需要使用 roundToPx() 从 dp 转换为 px)
               val paddingPx = 18.dp.roundToPx()
    
               // 2. 测量子组件:这里把可用的最大宽高都增加了 paddingPx * 2
               // 这样做会给子组件留出更大的测量空间,后续用来放置文字和可见的"空白"
               val placeable = measurable.measure(
                   constraints.copy(
                       maxWidth = constraints.maxWidth + paddingPx * 2,
                       maxHeight = constraints.maxHeight + paddingPx * 2
                   )
               )
    
               // 3. 调用 layout() 来确定最终的布局尺寸和放置逻辑
               // 宽度 = 子组件测量后的宽度 + paddingPx * 2
               // 高度 = 子组件测量后的高度 + paddingPx(示例里是 + paddingPx,不过也可根据需求加倍)
               layout(width = placeable.width + paddingPx * 2, height = placeable.height + paddingPx) {
                   // 将子组件放置在 (paddingPx, paddingPx) 位置
                   // 相对于左上角向右和向下各偏移 paddingPx
                   placeable.placeRelative(paddingPx, paddingPx)
               }
           }
       )
    }
  • 这个 Lambda 就充当了对测量与布局步骤的定义:

    1. 测量子组件 :通常我们需要调用 measurable.measure(constraints) 来测量子组件,得到一个 Placeable。
    2. 计算尺寸:根据子组件的尺寸和你需要添加的效果,计算最终所需要的宽度和高度。
    3. 布局 :在 layout(width, height) { ... } 的 block 中,指定子组件放置的位置(可以进行偏移或其他调整)。
1.2 典型用途
  • 自定义内边距或外边距:动态调整组件的 padding 或 margin 效果。
  • 复杂定位逻辑:例如根据某些条件调整子组件的相对位置,实现居中、偏移等效果。
  • 变换效果:在布局过程中应用旋转、缩放等简单变换(虽然较复杂的变换通常推荐使用 Modifier.graphicsLayer)。

2. 使用示例

下面通过一个简单示例说明如何使用 Modifier.layout() 来构造一个自定义的布局修饰符,该修饰符在原有布局基础上增加额外的偏移量。

kotlin 复制代码
@Composable
fun CustomLayoutModifierDemo() {
    // 自定义一个 Modifier,通过 layout() 修改子组件的位置,使其向右和向下偏移 16.dp
    val offsetModifier = Modifier.layout { measurable, constraints ->
        // 1. 测量子组件,相当于View.measure(),不过只能测量自己,不能测量子View
        val placeable = measurable.measure(constraints)
        
        // 2. 假设我们想要向右、向下偏移固定的 16.dp
        // 此处需要将 16.dp 转换成像素,可以使用 LocalDensity.current 或者传入已转换的值
        // 为了简单说明,假设 16.dp 已经转换为16像素
        val offsetX = 16
        val offsetY = 16
        
        // 3. 计算最终宽高(如果我们需要扩大尺寸以包含偏移区域,可以做额外计算,这里假设尺寸不变)
        // 如果不改变布局尺寸,只是对子组件进行位移,那么尺寸依旧为 placeable 的宽高
        layout(placeable.width, placeable.height) {// 保存测量结果,相当于setMeasureDimension()
            // 将子组件放置在 (offsetX, offsetY) 位置,这里边都是像素,不是Dp了。
            placeable.placeRelative(offsetX, offsetY) // 进行位置修改 相当于 执行onLayout()
        }
    }

    Box(
        modifier = Modifier
            .size(150.dp)
            .background(Color.LightGray)
            .then(offsetModifier)  // 应用自定义的布局修饰符
    ) {
        Text(
            text = "Hello Compose",
            modifier = Modifier.background(Color.Yellow)
        )
    }
}

传统View中测量与布局分别在两个方法中完成的,Compose也是这样的,只有把 placeable.这种调用放到lambda表达式才能把测量与布局切开。所以才会有下边这样的写法:

kotlin 复制代码
layout(placeable.width, placeable.height) {
    placeable.placeRelative(offsetX, offsetY)
}

而不能写成这样,将测量与布局放到一起执行:

kotlin 复制代码
layout(placeable.width, placeable.height) {
}
placeable.placeRelative(offsetX, offsetY)

另外需要注意,在传统的View中,onLayout 是精细对每一个子View进行摆放的,但是 Compose 的 layout() 方法则不是,只能对控件进行整体的摆放,任何的 Modifier 都不能实现传统 ViewGroup 中 onLayout 的效果,因为 Modifier 本来就是用来修饰具体的组件的。如果想精细的布局,应该使用 Layout 这个函数:

kotlin 复制代码
inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {

完整的自我测量在 Modifier.layout 中是做不到的,因为 Modifier 所做的只是把自己加在了目标组件上,作为一个Modifier 给添加上去,可是它并不像是 View 的 onMeasure 方法那样,是在 View 的内部,可以拿到 View 的各种信息,而 Modifier.layout 是无法拿到它对应的组件的各种属性的,这种信息缺失,就会导致它在做自我测量的时候不会像 onMeasure 那样自由,导致它不只是只能测量自己,而测不到内部组件,而且就算是测量自己也不能利用内部属性来做辅助测量,而只能测量一下原来的尺寸,来做修改。

只不过 Modifier.layout 能够实现一定的摆放能力,而 传统的 View 是不能这样的,他们必须把摆放放到 onLayout 中去做,所以 Modifier.layout 绝对不是和 View#onMeasure 等价的。Modifier.layout 除了能修改大小,顺便还能修改一下偏移。

示例讲解
  1. 定义 Modifier.layout Lambda

    offsetModifier 中,通过 Modifier.layout { measurable, constraints -> ... },创建一个自定义 LayoutModifier。

    • 通过 measurable.measure(constraints) 测量子组件,得到 placeable
    • 指定偏移量(这里示例中使用固定的 16 像素作为偏移值)。
  2. 返回最终布局尺寸与子组件位置

    • 调用 layout(placeable.width, placeable.height) { ... } 定义最终布局尺寸,保持与子组件相同(如果需要改变尺寸,也可以适当增加)。
    • layout 的 Lambda 中调用 placeable.placeRelative(offsetX, offsetY) 指定子组件的放置位置,从而实现向右与向下偏移。
  3. 使用自定义修饰符

    • 在 Box 的 modifier 中将自定义的偏移修饰符 offsetModifier 与其他修饰符(如 background、size)组合,从而达到修改布局行为的效果。

LayoutModifier 是 Jetpack Compose 中专门用于修改组件测量和布局过程的接口。它继承自 Modifier.Element,因此是构成 Modifier 链的一部分。通过实现或使用 LayoutModifier,我们可以在组件的测量和布局阶段介入,自定义它们的尺寸计算和子组件的放置逻辑。下面详细介绍其概念、内部工作原理以及如何使用。


LayoutModifier

先提一个问题:Modifier.padding(10.dp).padding(20.dp) 这样写,得到的结果是什么?Modifier.size(40.dp).requiredSize(80.dp) 的结果是什么?

1.1 定义与作用

  • 定义:
    LayoutModifier 是 Compose 中一种特殊的修饰符接口,用于改变目标组件的测量和布局行为。它可以影响子组件的测量约束、最终的尺寸以及内部子元素的摆放位置。

  • 作用:

    • 介入测量过程: 在调用子组件的测量方法之前或之后对测量约束进行处理,从而改变子组件的可测量区域。
    • 自定义布局逻辑: 在布局阶段对结果进行处理,例如对子组件进行偏移(类似于增加 padding)、调整排列方式等。
    • 增强可组合性: 利用 LayoutModifier,你可以创建具有通用布局效果的修饰符,并通过 Modifier 链方便复用到各类组件上。

1.2 工作原理

1.2.1 Compose 是如何进行测量和布局的?
kotlin 复制代码
override var modifier: Modifier = Modifier

LayoutNode#remeasure:测量
 -->measurePassDelegate.remeasure(constraints)
   -->LayoutNodeLayoutDelegate#performMeasure(constraints: Constraints)
     -->ModifiedLayoutNode#measure(constraints) // 真正完成实际测量的方法
       -->measureResult = measureScope.measure(wrapped, constraints) // 负责完成组件的测量,例如Text怎么测量,Box怎么测量,保存的测量结果用来在后边摆放控件用。
           
interface MeasureResult {
    val width: Int
    val height: Int
    val alignmentLines: Map<AlignmentLine, Int>
    fun placeChildren() // 该方法会在LayoutNode#replace被调用
}

LayoutNode#replace:布局,调用 placeChildren() 即可完成。

虽然有两个函数,一个是 LayoutNode#remeasure:测量,另一个是 LayoutNode#replace:布局,但是实际上,所有的核心工作在 LayoutNode#remeasure 就已经完成了,在 LayoutNode#remeasure中不仅完成了测量,还完成了应该怎么摆放的逻辑。LayoutNode#replace仅仅只是调用了 placeChildren()函数,把计算好的位置摆放就行了。

1.2.2 LayoutModifier 是如何影响测量和布局的?

它的工作原理,都包含在了 LayoutNode 里边的modifier属性里边了。LayoutNode 是在运行的时候,实际被 Text 、 Box 等各种 Composable 函数所生成的 代表每一个界面的节点。**我们对每一个 Composable 函数里边填写的 Modifier 参数,最终也会替换掉 LayoutNode 里边默认的 modifier 属性。 **

kotlin 复制代码
override var modifier: Modifier = Modifier
    set(value) {
        // 如果传入的新 modifier 与当前相同,则不做任何更新
        if (value == field) return

        // 如果当前 modifier 不是默认值(即已有 modifier 被设置过),
        // 并且当前 LayoutNode 是虚拟节点,则不支持设置 modifier,直接报错
        if (modifier != Modifier) {
            require(!isVirtual) { "Modifiers are not supported on virtual LayoutNodes" }
        }

        // 更新当前 modifier 字段
        field = value

        // 判断是否需要使父层的 Layer 失效(例如触发重绘)
        val invalidateParentLayer = shouldInvalidateParentLayer()

        // 将现有的 LayoutNodeWrapper 包装器复制到缓存中,以便后续重用
        copyWrappersToCache()
        // 清空所有包装器的实体状态,准备构建新的修饰符链
        forEachDelegateIncludingInner { it.entities.clear() }
        // 标记那些能够重用的 modifier(便于优化)
        markReusedModifiers(value)

        // 保存当前的 outerWrapper,便于后续判断是否发生了变化
        val oldOuterWrapper = layoutDelegate.outerWrapper

        // 如果存在外部 Semantics 信息并且节点已附着,则通知所有者发生语义变化
        if (outerSemantics != null && isAttached) {
            owner!!.onSemanticsChange()
        }

        // 检查是否有新增的位置回调,记录结果以便后续决定是否需要触发重新测量
        val addedCallback = hasNewPositioningCallback()
        // 清空已有的位置回调集合
        onPositionedCallbacks?.clear()

        // 初始化内层的 LayoutNodeWrapper,准备重新构建 modifier 链
        innerLayoutNodeWrapper.onInitialize()

        // 通过 modifier.foldOut,从内层(innerLayoutNodeWrapper)开始依次处理 modifier 链,
        // 构建新的 outerWrapper 链。foldOut 从"内"向"外"遍历每个 modifier。也就是从右往左依次应用。
        // mod 表示从右向左展开的时候,每一个modifier对象,例如这个,依次就是 
        // Modifier.padding().background(), background(), padding()
        // toWrap表示一层层处理然后返回的对象,对于下边的代码,初始值就是 innerLayoutNodeWrapper ,
        // innerLayoutNodeWrapper 是用来对原有内容进行测量的。
        // 例如这个代码中,就是对 "Hello World" 的测量:Text(text = "Hello World", modifier = Modifier.layout())
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
            // 如果当前 modifier 是 RemeasurementModifier,则通知其可用于重新测量
            if (mod is RemeasurementModifier) {
                mod.onRemeasurementAvailable(this)
            }

            // 在包装器实体中添加该 modifier 的"前置"操作(在布局前执行),也就是DrawModifier等。
            toWrap.entities.addBeforeLayoutModifier(toWrap, mod)

            // 如果当前 modifier 是 OnGloballyPositionedModifier,则添加其位置回调
            if (mod is OnGloballyPositionedModifier) {
                getOrCreateOnPositionedCallbacks() += toWrap to mod
            }

            // 如果当前 modifier 是 LayoutModifier,则尝试重用现有的包装器,
            // 否则创建一个新的 ModifiedLayoutNode,并进行初始化与 LookaheadScope 更新
            val wrapper = if (mod is LayoutModifier) {
                (reuseLayoutNodeWrapper(toWrap, mod)
                    ?: ModifiedLayoutNode(toWrap, mod)).apply {
                    onInitialize()
                    updateLookaheadScope(mLookaheadScope)
                }
            } else {
                // 非 LayoutModifier 直接复用当前包装器
                toWrap
            }
            // 在包装器实体中添加该 modifier 的"后置"操作(在布局后执行)
            wrapper.entities.addAfterLayoutModifier(wrapper, mod)
            // 返回包装器,用于下一层的 foldOut 处理
            wrapper
        }

        // 根据新的 modifier 更新 ModifierLocals 数据
        setModifierLocals(value)

        // 将构建后的 outerWrapper 与父节点的 innerLayoutNodeWrapper 关联
        outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
        // 更新当前 LayoutDelegate 中的 outerWrapper 引用
        layoutDelegate.outerWrapper = outerWrapper

        if (isAttached) {
            // 遍历缓存中的包装器,对于已被移除的包装器调用 detach() 方法
            wrapperCache.forEach {
                it.detach()
            }
            // 对每个 delegate(包括内层包装器):
            // 如果未附着则调用 attach(),如果已附着则对实体执行 onAttach() 回调
            forEachDelegateIncludingInner { layoutNodeWrapper ->
                if (!layoutNodeWrapper.isAttached) {
                    layoutNodeWrapper.attach()
                } else {
                    layoutNodeWrapper.entities.forEach { it.onAttach() }
                }
            }
        }
        // 清空包装器缓存
        wrapperCache.clear()

        // 遍历所有 delegate,通知它们 modifier 已变化
        forEachDelegateIncludingInner { it.onModifierChanged() }

        // 优化场景:
        // 1. 如果 outerWrapper 或 innerLayoutNodeWrapper 发生变化,则调用 invalidateMeasurements() 触发重测量
        // 2. 否则,如果当前布局处于空闲状态且没有测量挂起,但新增了位置回调,也需要重测量
        // 3. 或者,如果内层包装器中存在 OnPlacedEntity,则确保调用其 onPlaced 回调
        if (oldOuterWrapper != innerLayoutNodeWrapper ||
            outerWrapper != innerLayoutNodeWrapper
        ) {
            invalidateMeasurements()
        } else if (layoutState == Idle && !measurePending && addedCallback) {
            invalidateMeasurements()
        } else if (innerLayoutNodeWrapper.entities.has(EntityList.OnPlacedEntityType)) {
            owner?.registerOnLayoutCompletedListener(this)
        }
        // 如果父数据发生更改,则通知父节点需要重新测量
        layoutDelegate.updateParentData()
        // 如果需要使父层渲染层失效,则通知父节点进行更新
        if (invalidateParentLayer || shouldInvalidateParentLayer()) {
            parent?.invalidateLayer()
        }
    }

对于这一块逻辑的更深入的解释:

kotlin 复制代码
    val wrapper = if (mod is LayoutModifier) {
        (reuseLayoutNodeWrapper(toWrap, mod)
            ?: ModifiedLayoutNode(toWrap, mod)).apply {
            onInitialize()
            updateLookaheadScope(mLookaheadScope)
        }
    } else {
        // 非 LayoutModifier 直接复用当前包装器
        toWrap
    }
    // 在包装器实体中添加该 modifier 的"后置"操作(在布局后执行)
    wrapper.entities.addAfterLayoutModifier(wrapper, mod)
    // 返回包装器,用于下一层的 foldOut 处理
    wrapper
}
// 根据新的 modifier 更新 ModifierLocals 数据
setModifierLocals(value)
// 将构建后的 outerWrapper 与父节点的 innerLayoutNodeWrapper 关联
outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
// 更新当前 LayoutDelegate 中的 outerWrapper 引用
layoutDelegate.outerWrapper = outerWrapper

更形象的表示其实就相当于是下边这种:

kotlin 复制代码
不设置 Modifier:
outerWrapper =innerLayoutNodeWrapper->测量Composable 函数,比如 Text()

设置了一个 LayoutModifier: Modifier.layout { ... }
outerWrapper =
ModifierLayoutNode[
    LayoutModifier
    +
    innerLayoutNodeWrapper -> 测量 Composable 函数,比如 Box()
]

如果设置 两个 LayoutModifier(例如PaddingModifier等):
outerWrapper =
ModifierLayoutNode[
    LayoutModifier
    +
    ModifierLayoutNode[
        LayoutModifier
        +
        innerLayoutNodeWrapper -> 测量 Composable 函数,比如 Text()
    ]
]

因此,从上边的代码中我们就能知道 Compose 是如何运用在我们自己的代码中自定义Modifier.layout的逻辑了,其实就是一个替换,我们所有的逻辑都会在包装之后,替换掉默认的LayoutNodeLayoutDelegate#outerWrapper 对象,在 ModifiedLayoutNode又是如何完成测量的呢?

kotlin 复制代码
internal class ModifiedLayoutNode(
    override var wrapped: LayoutNodeWrapper, // innerLayoutNodeWrapper -> 测量 Composable 函数,比如 Text()
    var modifier: LayoutModifier // LayoutModifier
)

override fun measure(constraints: Constraints): Placeable {
    performingMeasure(constraints) {
        with(modifier) {
            // wrapped 的来源是ModifiedLayoutNode 的 wrapped参数,这个参数其实就是 innerLayoutNodeWrapper -> 测量 Composable 函数,比如 Text() 
            measureResult = measureScope.measure(wrapped, constraints)
            this@ModifiedLayoutNode
        }
    }
    onMeasured()
    return this
}

protected inline fun performingMeasure(
    constraints: Constraints,
    block: () -> Placeable
): Placeable {
    measurementConstraints = constraints
    val result = block()
    layer?.resize(measuredSize)
    return result
}

它的内部调用了performingMeasure,不过并不是关键,因为它还是调用了传入的block,这个block就是我们自定义的Modifier.layout逻辑,关键在于with(modifier)里边的逻辑,以及 measureScope.measure(wrapped, constraints), 它的来源在LayoutModifier中,这种写法的意思就是:只有处于LayoutModifier这样一个上下文环境,才能调用measure方法。LayoutModifier的上下文又是怎么被提供出来的呢?靠的就是这个 with(modifier) 所提供的上下文。也就是说measureScope.measure的实现其实是由(modifier)决定的,我们怎么写Modifier.layout内部的实现逻辑, measureScope.measure(wrapped, constraints)就会怎么工作。

LayoutModifier的实现类是由LayoutModifierImpl,因此上图中的

的具体实现也在LayoutModifierImpl中:

弄清楚原理之后,我们就可以解释 Modifier.padding(10.dp).padding(20.dp)得到的结果是多少了。其实就是30dp,第一层增加10dp,第二层增加20dp,最后的 innerLayoutNodeWrapper 不再有内部了,所以它会做实际的测量。另外,针对其他的一些问题的解答:

kotlin 复制代码
Modifier.padding(10.dp).padding(20.dp) 的结果是什么?

Text("example", Modifier.padding(10.dp).padding(20.dp))
Box(Modifier.padding(10.dp).padding(20.dp))

[LayoutModifier ~10.dp  ModifiedLayoutNode
    [LayoutModifier ~20.dp  ModifiedLayoutNode
        实际的组件 Text(), Box()  innerLayoutNodeWrapper -> InnerPlaceable
    ]
]

同样,根据Compose的绘制原理,我们也能知道:

Modifier.size(40.dp).size(20.dp) 的结果是什么?
最终的大小是40dp

Modifier.size(40.dp).size(80.dp) 的结果是什么?
最终的大小是40dp

Modifier.size(40.dp).requiredSize(80.dp) 的结果是什么?
最终的显示的大小是40dp,因为父布局的大小就是40dp,但是里边的requiredSize(80.dp) 确实绘制了80dp的大小,如果是一张图片,可以看到只有左上角的部分被裁剪出来了。

Modifier.size(80.dp).requiredSize(40.dp) 的结果是什么?
整体的的大小还是80dp,只不过实际有效的显示的区域只有40dp,然后被居中摆放。如下图所示

关于几个重点参数的解释现在就可以更好的理解了:

kotlin 复制代码
internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
internal val layoutDelegate = LayoutNodeLayoutDelegate(this, innerLayoutNodeWrapper)
internal val outerLayoutNodeWrapper: LayoutNodeWrapper
    get() = layoutDelegate.outerWrapper
  • innerLayoutNodeWrapper :封装了直接对 Composable 内容(例如 Text)的测量和布局,代表内层测量/放置逻辑,用来负责测量Composable函数,所给出的算法的对象。也就是后边所说的Modifier.layout { measurable, constraints -> 这个 measurable 参数其实就是 innerLayoutNodeWrapper ,当然这个只是单层的 Modifier.layout, 如果嵌套多层的,则对象就是 ModifierLayoutNode 了,所以说 measurable 的类型是不定的。

  • layoutDelegate:作为协调器,利用当前节点和 innerLayoutNodeWrapper 处理 Modifier 链的更新、包装器的重建,并管理测量、布局、缓存和状态通知;

  • outerLayoutNodeWrapper:通过 layoutDelegate 得到的最终包装器,代表组合后的、对外呈现的布局包装,包含了所有应用的布局修改逻辑。

如何实现多个layout 函数的调用? 在下边的例子中,为 Box 组件应用了两个自定义的 layout 修饰符:

kotlin 复制代码
@Composable
fun TwoLayoutModifiersExample() {
    Box(
        modifier = Modifier
            // 第一个 LayoutModifier,相当于外层的包装器(outerWrapper)
            .layout { measurable, constraints ->
                // 这里定义一个外层 padding,比如 20dp
                val outerPadding = 20.dp.roundToPx()
                // 测量子组件,直接传入原始 constraints
                val placeable = measurable.measure(constraints)
                // 返回一个更大的尺寸,给子组件左右上下各增加 outerPadding
                layout(
                    width = placeable.width + outerPadding * 2,
                    height = placeable.height + outerPadding * 2
                ) {
                    // 将子组件放置在 outerPadding 的偏移位置处
                    placeable.placeRelative(outerPadding, outerPadding)
                }
            }
            // 第二个 LayoutModifier,相当于内层包装器
            .layout { measurable, constraints ->
                // 定义一个内层 padding,比如 10dp
                val innerPadding = 10.dp.roundToPx()
                // 测量子组件,传入原始 constraints
                val placeable = measurable.measure(constraints)
                // 返回比原有尺寸稍大的尺寸,以便增加内层 padding
                layout(
                    width = placeable.width + innerPadding * 2,
                    height = placeable.height + innerPadding * 2
                ) {
                    // 将子组件放置在 innerPadding 的偏移位置处
                    placeable.placeRelative(innerPadding, innerPadding)
                }
            },
        contentAlignment = Alignment.Center
    ) {
        // 内部 Composable,比如 Text 会先被第二个 Modifier 处理,再被第一个 Modifier 处理,
        // 最终效果是文本外会有两个层次的额外空间,外层 20dp、内层 10dp
        Text(text = "Hello, Two LayoutModifiers!")
    }
}
  • 插入一层"中间层":

    当我们对组件链中添加了一个 LayoutModifier(例如通过调用 Modifier.layout { measurable, constraints -> ... }),实际上 Compose 框架会在父布局传递的约束和子组件实际测量之间插入这一层。这个中间层可以:

    • 修改传入子组件的测量约束;
    • 调用子组件的 measure() 方法得到一个 Placeable 对象;
    • 再次调整最终返回的尺寸,并在布局阶段指定子组件的具体摆放位置。
  • 交互模式:

    一般来说,LayoutModifier 的使用场景包括:

    • 为组件增加额外的内边距或外边距;
    • 对子组件的位置进行偏移、居中或其他对齐方式的调整;
    • 执行其他特殊布局逻辑,比如根据内容动态调整尺寸。

2. 使用方式

通常,开发者并不需要直接实现 LayoutModifier 接口,而是利用 Compose 提供的便捷扩展函数------Modifier.layout { measurable, constraints -> ... } 来定义自定义的布局逻辑。使用这种方式的好处包括:

  • 简化代码: 所有测量和布局逻辑集中在一个闭包内;
  • 声明式表达: 你只需要描述"怎么测量,怎么布局",而不必关心复杂的继承和生命周期问题;
  • 复用性: 自定义的布局修饰符可以作为普通 Modifier 的一部分,通过链式组合与其他修饰符叠加使用。

3. 使用示例

下面通过一个示例展示如何用 Modifier.layout 来实现为组件增加 padding 的功能。

kotlin 复制代码
@Composable
fun CustomPaddingExample() {
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .layout { measurable, constraints ->
                // 定义 padding 大小(将 16.dp 转换为像素)
                val padding = 16.dp.roundToPx()

                // 调整传递给子组件的约束:
                // 这里我们减少可用尺寸,目的是让子组件测量时忽略掉 padding 部分
                // 注意:这是一种常见的方案,也可以反过来:测量时让子组件获得较小的区域,
                // 然后再扩大最终容器的尺寸来容纳 padding(取决于你的效果需求)
                val modifiedConstraints = constraints.offset(-padding * 2, -padding * 2)

                // 测量子组件
                val placeable = measurable.measure(modifiedConstraints)

                // 返回最终容器的尺寸:原子组件测量的尺寸再加上 padding
                layout(
                    width = placeable.width + padding * 2,
                    height = placeable.height + padding * 2
                ) {
                    // 在布局阶段,将子组件放置在 (padding, padding) 的位置
                    placeable.placeRelative(padding, padding)
                }
            }
    ) {
        Text(text = "Hello, LayoutModifier!", modifier = Modifier.background(Color.Yellow))
    }
}

示例解析

  1. Box 容器与背景设置:

    使用 Box 作为容器,并应用了 Modifier.background(Color.LightGray),从而可以直观地看到自定义修饰符的效果。

  2. 自定义 layout 修饰符:

    kotlin 复制代码
    Modifier.layout { measurable, constraints -> ... }

    这行代码创建了一个匿名的 LayoutModifier 对象,它可以拦截测量和布局过程。

    • 测量阶段:

      • 定义 padding 值,将 16.dp 转换为像素。
      • 使用 constraints.offset(-padding * 2, -padding * 2) 调整约束,让子组件可以在扣除 padding 后测量。
      • 调用 measurable.measure(modifiedConstraints) 得到 Placeable 对象。
    • 布局阶段:

      • 返回最终容器的宽高为 子组件的宽高 + padding * 2(即在左右和上下各增加 padding)。
      • layout {} 的 lambda 中,调用 placeable.placeRelative(padding, padding) 将子组件放置在 (padding, padding) 的位置,从而实现内边距效果。
  3. 结果效果:

    最终,子组件会获得额外的内边距,视觉上看起来文本周围有 16.dp 的间距,并且外层 Box 的尺寸也相应增大。这正是通过 LayoutModifier 对布局行为进行自定义实现的效果。


4. 总结

分析完原理之后,我们也能明白,下边这两种写法效果是完全一致的,第一种写法仅仅是对后者的一个简洁封装,从功能和结果来看,它们是没有区别的。:

kotlin 复制代码
Modifier.layout { measurable, constraints ->  }

Modifier.then(object :LayoutModifier{
  override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
  }
})
  • 核心优势:

    通过 LayoutModifier,我们可以灵活地介入 Compose 的测量和布局流程,以函数式、声明式的方式描述"如何测量"和"如何布局"。

  • 使用场景:

    • 为组件添加自定义的 padding 或 margin 效果;
    • 对子组件进行位置偏移或对齐调整;
    • 实现一些特殊的布局需求,如动态尺寸调整、内容居中等。
  • 注意事项:

    修改后的布局尺寸可能会影响父容器(例如在父布局使用 wrap_content 的情况下),因此在设计时需要考虑如何平衡子组件与父布局之间的尺寸约束。

DrawModifier

布局修饰符与绘制修饰符的区别

  • 布局修饰符(如 padding、size)会影响组件的测量和布局结果。顺序不同,会导致内容的可用空间和最终尺寸产生变化。

  • 绘制修饰符 (如 background),它的范围大小取决于它后边的第一个Modifier的大小,例如下边,将会有个80dp的蓝色区域:

    kotlin 复制代码
    Box(Modifier.background(Color.Blue).padding(80.dp).size(100.dp))

下边的代码这将会产生一个40dp的蓝色区域:

kotlin 复制代码
Box(Modifier.background(Color.Blue).padding(80.dp).size(100.dp))

1. DrawModifier 的原理

1.1 基本概念

  • 接口定义:

    DrawModifier 是一个接口,通常继承自 Modifier.Element。其核心方法在于一个带有 DrawScope 接收者的绘制函数,例如:

    kotlin 复制代码
    interface DrawModifier : Modifier.Element {
        fun DrawScope.draw()
    }

    在这个 draw() 方法中,你会写下自定义的绘制逻辑。

  • 绘制流程:

    在 Compose 的绘制流程中,每个 LayoutNode 会遍历它的 Modifier 链。如果其中某个 Modifier 实现了 DrawModifier 接口,Compose 就会在绘制阶段调用它的 draw() 方法。这个过程通常是从外到内的,即最外层的 DrawModifier 会最先插手,调用它的 draw 方法时可决定是否调用 drawContent() 来委托内部内容的绘制。

  • DrawScope:

    draw() 方法的接收者是 DrawScope,该作用域提供了 Canvas、绘制 API(例如 drawRect、drawLine、drawImage 等),以及当前绘制区域的尺寸信息。通过 DrawScope,我们可以方便地绘制各种图形和效果。

1.2 内部实现

1.2.1 内部实现原理

它的作用不是去增加内容,而是替换绘制内容。所以如果想保留原来的绘制,需要保留drawContent(),否则内部内容会被擦掉。 同 layout{} 一样,DrawModifier 也有自己的便捷函数,下边两种写法效果也是一样的

kotlin 复制代码
Modifier.drawWithContent { drawContent()// 不调用这个方法会导致之前的内容都被擦出掉}

Modifier.then(object : DrawModifier{
  override fun ContentDrawScope.draw() {
     drawContent() // 不调用这个方法会导致之前的内容都被擦出掉
  }
})
  • 组合与链式调用:

    DrawModifier 通常以 Modifier.drawBehind、Modifier.drawWithContent 等扩展函数的形式出现,这些扩展函数内部创建了实现 DrawModifier 的对象。当我们像这样使用:

    kotlin 复制代码
    Modifier.drawBehind { 
        // 在默认内容绘制之前绘制蓝色矩形,Compose的canvas不能绘制Text
        drawRect(Color.Blue, size = size)
        drawContent() // 然后绘制原有内容
    }

    Compose 实际上创建了一个 DrawModifier 对象,并把这个对象插入到 Modifier 链中。整个 Modifier 链在绘制时,会依次调用各个 DrawModifier 的 draw() 方法,从而产生层叠的绘制效果。

  • 调用 drawContent():

    在某些情况下(例如使用 Modifier.drawWithContent),我们可能希望在自定义绘制之前或之后调用默认的绘制逻辑。此时可以在 draw() 方法中调用 drawContent() 来让子组件按照默认逻辑绘制,然后再加上额外的绘制操作。

  • 重组与绘制更新:

    当涉及 DrawModifier 的属性改变时,Compose 会判断是否需要重新执行绘制操作。如果你的 DrawModifier 使用了状态,当状态变化时会自动触发重组(recomposition),从而调用新的 draw() 方法,更新绘制效果。


1.2.2源码分析

为了更深入的了解 DrawModifier 是如何影响 Compose 的绘制流程的,是时候看看它内部的源码实现了。

kotlin 复制代码
override var modifier: Modifier = Modifier
        //......省略其他代码
        // Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers
        // when possible.
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
            if (mod is RemeasurementModifier) {
                mod.onRemeasurementAvailable(this)
            }
            // 在包装器实体中添加该 modifier 的"前置"操作(在布局前执行),也就是DrawModifier等。
            toWrap.entities.addBeforeLayoutModifier(toWrap, mod)

            if (mod is OnGloballyPositionedModifier) {
                getOrCreateOnPositionedCallbacks() += toWrap to mod
            }
            // 处理LayoutModifier
            val wrapper = if (mod is LayoutModifier) {
                // Re-use the layoutNodeWrapper if possible.
                (reuseLayoutNodeWrapper(toWrap, mod)
                    ?: ModifiedLayoutNode(toWrap, mod)).apply {
                    onInitialize()
                    updateLookaheadScope(mLookaheadScope)
                }
            } else {
                toWrap
            }
            wrapper.entities.addAfterLayoutModifier(wrapper, mod)
            wrapper
        }
        //......省略其他代码
        
// 按照不同的类型,将Modifier添加到数组的固定位置。每不同类型的Modifier在数组中的位置是确定的,同一类型的Modifier需要通过链表串联起来。
fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
    if (modifier is DrawModifier) {
        add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index)
    }
    if (modifier is PointerInputModifier) {
        add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index)
    }
    if (modifier is SemanticsModifier) {
        add(SemanticsEntity(layoutNodeWrapper, modifier), SemanticsEntityType.index)
    }
    if (modifier is ParentDataModifier) {
        add(SimpleEntity(layoutNodeWrapper, modifier), ParentDataEntityType.index)
    }
}

// 这个数据结构式一个数组+链表的组合,数组里边存储的都是链表的头节点,通过这个头节点可以范围同一类型的所有Modifier, 新加入的元素回作为头节点。
private fun <T : LayoutNodeEntity<T, *>> add(entity: T, index: Int) {
    @Suppress("UNCHECKED_CAST")
    val head = entities[index] as T?
    entity.next = head
    entities[index] = entity
}

internal value class EntityList(
    val entities: Array<LayoutNodeEntity<*, *>?> = arrayOfNulls(TypeCount)
) {
private const val TypeCount = 7

经过上边的流程,处理完成后的LayoutNodeWrapper#entities大概是这样的(如果都存在的话,不存在当然就是 null 了。):

kotlin 复制代码
[
   LayoutNodeEntity(LayoutNodeWrapper, DrawModifier2) -> LayoutNodeEntity(LayoutNodeWrapper, DrawModifier1),
   PointerInputModifier(LayoutNodeWrapper, PointerInputModifier )
   null, 
   SimpleEntity(LayoutNodeWrapper, SimpleEntity3) -> SimpleEntity(LayoutNodeWrapper, SimpleEntity2) -> SimpleEntity(LayoutNodeWrapper, SimpleEntity1),
   OnPlacedModifier(LayoutNodeWrapper, OnPlacedModifier2) -> OnPlacedModifier(LayoutNodeWrapper, OnPlacedModifier1),
   null, 
   LookaheadOnPlacedModifier(LayoutNodeWrapper, LookaheadOnPlacedModifier2) -> LookaheadOnPlacedModifier(LayoutNodeWrapper, LookaheadOnPlacedModifier1),
]

当所有的信息处理完成之后,将会调用下边这个方法进行后续的绘制流程。

kotlin 复制代码
internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
// layoutDelegate.outerWrapper的初始值就是outerLayoutNodeWrapper,
// 因为:val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper)
// outerWrapper的值也有可能被替换,在遍历Modifier的过程中,每次遇到一个 LayoutModifier,就会在外边包装一层 ModifiedLayoutNode, 
// ModifiedLayoutNode 里边包含了新加入的 LayoutModifier ,也就是说 outerLayoutNodeWrapper 是一个附加了我们自己测量算法的,
// 也就是带有我们添加的 LayoutModifier 的测量算法的更完整的测量工具,它不止包含innerLayoutNodeWrapper, 也包含了我们添加的 LayoutModifier , 所以 outerLayoutNodeWrapper 不只是用来测量,还用来做绘制。
internal val outerLayoutNodeWrapper: LayoutNodeWrapper
    get() = layoutDelegate.outerWrapper的值也有可能被替换
    

/**
 * 传统的View中可以使用:view.setLayerType(View.LAYER_TYPE_SOFTWARE, Paint()) 来设置type,
 * 底层使用GPU 使用。另一个就是canvas.saveLayer()为调用的canvas开辟一个临时的绘制区,
 * 这个区域会在绘制完成后调用 canvas.restore() 或者 cavas.restoreToCount(0) 会被贴回到View的所在区域。
 */
fun draw(canvas: Canvas) {
    val layer = layer // 安卓 10 以上使用的是 RenderNode 实现的。
    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) // 仅仅只是位移
    }
}

compose 里边的 drawLayer 有点类似于 View 的 setLayerTypesetLayerType 会把绘制的内容放到持有图层里边,背后其实就是给这个 View "绑" 上了一个离屏图层(off‑screen layer) ,所有原本直接在屏幕 Canvas 上的绘制,都先被导向到这个图层里,最后再一次性贴回到真正显示的屏幕上。同样的,Compose 的drawLayer 也是把Compose界面结构的的子内容放进单独的图层。它的作用之一也和setLayerType,就是互相独立刷新的,那么有时候就可以降低重复刷新的绘制时间。另外一个作用就是可以对独立绘制的图层的内容做简单操作,这是传统 View 的 setLayerType 不具备的能力,就是 放大、缩小、移动、透明度、切边等等。在Compose里边,所有的 东西都是在同一个 View 里边,所以无法直接做切边、修改透明度这些,因为 View 不能做分层。否则会造成整个一块全部被修改, 而有了 Layer ,就相当于我们添加了一个分层。相当于我们让父 View 和 子 View 有了分层的关系。这样我们就可以只对每一个分层做处理了,因此,简单总结一下:layer 除了可以独立绘制(一个独立绘制的图层),独立刷新之外,还有一个重要的作用就是分层隔离。 Modifier.alpha() Modifier.clip() 他们内部就是利用 layer 实现的。当然 layer 可能有也可能没有,而且大多数时候也是没有的。也不用想太多,它就是在一个独立的地方绘制罢了。

无论是老 View 体系里的 setLayerType,还是 Compose 里的 drawLayer(或更常见的 graphicsLayer),它们本质上都是在背后开辟了一个「离屏图层/off‑screen layer」,把后续的绘制先导到这个图层上,再统一合成到主画布。这样带来的好处有两方面:

  1. 独立绘制、独立刷新

    • View+setLayerType :开了硬件图层后,这个 View 的内容会被缓存成一个 GPU 纹理(或软件图层的 Bitmap),多帧之间复用,不会每次都重新走完整个 onDraw。做移动/旋转/透明度动画时,直接复用这个纹理即可,性能大幅提升。
    • Compose+drawLayer/graphicsLayer:也是开了一个 RenderNode 图层(也是 GPU 纹理或 Skia layer),Compose 可以只重绘图层内容,而不是整颗 UI 树。
  2. 分层隔离(分组变换)

    • 在开启了 layer 的情况下,你就可以对这一层做独立的变换:平移、缩放、旋转、透明度、裁剪(clip)、阴影、Xfermode 等等。
    • 没有 layer 的话,对整个 Canvas 或整个 View 只能一次性做,不可能只对子集做这些操作。
    • 在 View 里,setLayerType 后你还是只能对整个 View 做一次性动画或 alpha,不像 Canvas 的 saveLayer + Xfermode 那样可以在一次 draw() 里做更细粒度的蒙版裁剪。但在 Compose 里,graphicsLayer 不仅能做整体动画,也能配合 Modifier.clip()Modifier.alpha() 等在单层内完成局部裁剪/透明度,而不影响兄弟节点。

  • 相同点

    • 都是离屏图层(off‑screen buffer),先绘到图层再合成到屏幕。
    • 都能减少重复绘制、实现更高效的动画和重绘控制。
  • 不同点

    • 老 ViewsetLayerType 作用于整个 View,跨帧缓存;而 canvas.saveLayer() 又是另一个临时离屏,作用域限于一次 draw()
    • ComposeModifier.graphicsLayer(或 drawLayer)相当于 View 的 setLayerType(HARDWARE)------给那段子树开一个持久 RenderNode;而在需要做类似 saveLayer + Xfermode 的蒙版时,Compose 下你也可以用 drawIntoCanvas { it.saveLayer(...) }

不过在 Compose 里,我们平时用的 Modifier.alpha()Modifier.clip()Modifier.shadow()......它们内部会按需自动帮你开 layer,所以你一般不用手动管理,除非有非常复杂的 Xfermode 或自定义离屏效果需要自己去saveLayer

kotlin 复制代码
Modifier.alpha()
Modifier.clip()

@Stable
fun Modifier.alpha(
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float
) = if (alpha != 1.0f) graphicsLayer(alpha = alpha, clip = true) else this

@Stable
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

继续分析源码:drawContainedDrawModifiers(canvas), 它的内部实现是这样的:

kotlin 复制代码
private fun drawContainedDrawModifiers(canvas: Canvas) {
    val head = entities.head(EntityList.DrawEntityType)
    // 是否至少设置过一个DrawModifier
    if (head == null) {
        performDraw(canvas)
    } else {
        head.draw(canvas)
    }
}


fun <T : LayoutNodeEntity<T, M>, M : Modifier> head(entityType: EntityType<T, M>): T? =
    entities[entityType.index] as T?

head是否为空表示是否至少设置过一个 DrawModifier, 设置了当然需要把 DrawModifier 和原有内容一起绘制,没有设置,只绘制原有内容就可以了。根据前边的分析,我们知道,默认没有Modifier的时候, outerLayoutNodeWrapper 这个负责布局会绘制的变量的默认值就是 InnerPlaceable, 而一旦我们添加了其余的Modifier的属性,就会变成这样:

kotlin 复制代码
// 默认
outerLayoutNodeWrapper:
InnerPlaceable

Box(Modifier.padding(8.dp).size(40.dp))

outerLayoutNodeWrapper:
ModifiedLayoutNode(
    PaddingModifier,
    ModifiedLayoutNode(有测量绘制功能
        SizeModifier,
        InnerPlapeable // 有测量绘制功能, 这个是系统自带的,其余的都需要自己实现。
    )
)
// 在 Compose 布局/绘制链中,每个 `ModifiedLayoutNode`(或者更广义的任何 `LayoutNodeWrapper`)都有一个可选的 `wrapped` 引用
outerWrapper 
   └─ wrapped → middleWrapper
         └─ wrapped → innerWrapper
               └─ wrapped → null    ← 最里层,没有更多 ModifiedLayoutNode
我们先来分析一下head.draw(canvas)
kotlin 复制代码
open fun performDraw(canvas: Canvas) {
     wrapped?.draw(canvas) // 为什么 wrapped 可能为空
}

为什么 在 performDraw 方法中 wrapped 可能为空呢? 为空就表示 外层没有 ModifiedLayoutNode ,什么意思?比如上边的代码中,InnerPlapeable 就是 ModifiedLayoutNode 的 wrapped ,而下边的 ModifiedLayoutNode 就是第一个 ModifiedLayoutNode 的 wrapped。InnerPlapeable 里边没有别的 ModifiedLayoutNode, wrapped == null 表示"再往里(inner)没有 ModifiedLayoutNode"。所以 使用 InnerPlapeable 的时候 wrapped 为空。因此,当headl == null 的时候,表示当前层没有设置过 DrawModifier,就会让下一层执行 wrapped?.draw(canvas),全程都没有设置DrawModifier的话,就是层层递进执行了:

kotlin 复制代码
ModifiedLayoutNode(// 检查这一层是否有设置 DrawModifier
    PaddingModifier,
    [DrawModifier2 -> DrawModifier1, null, null, null, null, null, null]
    ModifiedLayoutNode(// 检查这一层是否有设置 DrawModifier
        SizeModifier,
        [DrawModifier3, null, null, null, null, null, null]
        InnerPlaceable(// 有测量绘制功能, 这个是系统自带的,其余的都需要自己实现。
            [DrawModifier4 -> DrawModifier5 -> DrawModifier6, null, null, null, null, null, null]
        )
    )
)

但是我们也发现了一个问题,如果外层都不写任何Modifier的话,系统的组件是怎么完成绘制的呢?因为默认的wrapped 都为空,就不会去触发 draw 函数。其中测量与布局最底层用的是这个函数,不过该函数并没有提供绘制功能:

kotoin 复制代码
@UiComposable
@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit, // 定制的测量和布局的算法,所谓的原有的布局好算法, InnerPlaceable 也就是利用这个lambda 表达式完成这个操作的。LayoutModifier 也可以在外部通过对该结果的进一步修改完成定制化。
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
        },
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

为什么不设置 DrawModifier 的时候就不会进行绘制? 这是因为系统底层的实现是这样的,所有的控件最终都是通过 DrawModifier 完成了绘制,不存在系统默认的绘制。从源码也能看出:Compse的底层是没有提供绘制能力的。只有测量与布局。我们用到的系统控件都是利用 DrawModifier 来实现绘制的。

再看看 DrawModifier 不为空的时候 performDraw(canvas)
kotlin 复制代码
head.draw(canvas) // 开始绘制流程

// This is not thread safe
fun draw(canvas: Canvas) {
    val size = size.toSize()
    if (cacheDrawModifier != null && invalidateCache) {
        layoutNode.requireOwner().snapshotObserver.observeReads(
            this,
            onCommitAffectingDrawEntity,
            updateCache
        )
    }

    val drawScope = layoutNode.mDrawScope
    drawScope.draw(canvas, size, layoutNodeWrapper, this) { // 代码块就是后续使用block
        with(drawScope) {
            with(modifier) {
                draw() // 这里就是绘制我们自定义的绘制逻辑了。这样就是了为什么自定义的时候,不调用 drawContent 就会把原有内容都丢掉。因为Compose 内部真的没有对这块做任何特殊处理。
            }
        }
    }
}

internal inline fun draw(
    canvas: Canvas,
    size: Size,
    layoutNodeWrapper: LayoutNodeWrapper,
    drawEntity: DrawEntity,
    block: DrawScope.() -> Unit
) {
    val previousDrawEntity = this.drawEntity
    this.drawEntity = drawEntity
    canvasDrawScope.draw(
        layoutNodeWrapper.measureScope,
        layoutNodeWrapper.measureScope.layoutDirection,
        canvas,
        size,
        block
    )
    this.drawEntity = previousDrawEntity
}

inline fun draw(
    density: Density,
    layoutDirection: LayoutDirection,
    canvas: Canvas,
    size: Size,
    block: DrawScope.() -> Unit
) {
    // Remember the previous drawing parameters in case we are temporarily re-directing our
    // drawing to a separate Layer/RenderNode only to draw that content back into the original
    // Canvas. If there is no previous canvas that was being drawing into, this ends up
    // resetting these parameters back to defaults defensively
    val (prevDensity, prevLayoutDirection, prevCanvas, prevSize) = drawParams
    drawParams.apply {
        this.density = density
        this.layoutDirection = layoutDirection
        this.canvas = canvas
        this.size = size
    }
    canvas.save()
    this.block()
    canvas.restore()
    drawParams.apply {
        this.density = prevDensity
        this.layoutDirection = prevLayoutDirection
        this.canvas = prevCanvas
        this.size = prevSize
    }
}

不过我们也发现了,只有 head.draw() ,那么其他的 DrawModifier 应该怎么办呢? 这就需要通过 DrawContent来实现了。

kotlin 复制代码
override fun drawContent() {
    drawIntoCanvas { canvas ->
        val drawEntity = drawEntity!!
        val nextDrawEntity = drawEntity.next
        if (nextDrawEntity != null) { // 下一个DrawEntity,还有没有下一个DrawModifier
            nextDrawEntity.draw(canvas)
        } else { // 只有一个DrawModifier的情况,也就是只有一个头节点。
            drawEntity.layoutNodeWrapper.performDraw(canvas)
        }
    }
}

inline fun DrawScope.drawIntoCanvas(block: (Canvas) -> Unit) = block(drawContext.canvas)

ModifiedLayoutNode(
    [DrawModifier2 -> DrawModifier1, null, null, null, null, null, null] // 不调用drawContent,后边的都无法绘制。
    ModifiedLayoutNode(
        PaddingModifier,
        [DrawModifier3, null, null, null, null, null, null]
        InnerPlaceable(
            [DrawModifier4->DrawModifier5->DrawModifier6, null, null, null, null, null, null]
        )
    )
)

// 对应的绘制流程就是:
```kotlin
DrawModifier2.draw() {
    //... 绘制代码可以在这里。
    drawContent() {
        DrawModifier1.draw() {
            drawContent() {
                DrawModifier3.draw() {
                    drawContent() {
                        DrawModifier4().draw() {
                            drawContent() {
                                DrawModifier5().draw(){
                                    ...
                                }
                            }
                        }
                    }
                }
            }
        }
    }
   //... 绘制代码也可以在这里,会导致覆盖的方式不同。
}

从源码流程我们知道了:不调用 DrawContent ,会让除了当前 ModifierLayoutNode 后边的所有布局全部不会绘制,因此,一定要记得在 DrawModifier 中调用 drawContent 函数 , 除了 DrawModifier2, 其余的 DrawModifier 都是在 drawContent 这个函数中被调用的。

前边我们讲的都是 layer 为空的时候的绘制,那么当 layer不为空的时候怎么做的呢?

kotlin 复制代码
if (layer != null) {
    layer.drawLayer(canvas)
}

androidx.compose.ui.node.OwnedLayer#drawLayer

androidx.compose.ui.platform.RenderNodeLayer#drawLayer

override fun drawLayer(canvas: Canvas) {
        // 省略其他代码...
        drawBlock?.invoke(canvas) // 主要看这个。
        canvas.restore()
        isDirty = false
    }
}

// 下边就是drawBlock 的具体实现:
androidx.compose.ui.node.LayoutNodeWrapper#invoke
override fun invoke(canvas: Canvas) {
    if (layoutNode.isPlaced) {
        snapshotObserver.observeReads(this, onCommitAffectingLayer) {
            drawContainedDrawModifiers(canvas) // 还是回到了之前的调用方式了。
        }
        lastLayerDrawingWasSkipped = false
    } else {
        // The invalidation is requested even for nodes which are not placed. As we are not
        // going to display them we skip the drawing. It is safe to just draw nothing as the
        // layer will be invalidated again when the node will be finally placed.
        lastLayerDrawingWasSkipped = true
    }
}

实现类有两个,我们上边只是抽取一个讲解,但本质上还是通过后边的 drawContainedDrawModifiers 函数触发的。

一些示例讲解
kotlin 复制代码
// 红色是蓝色的背景。
Box(Modifier.background(Color.Red).background(Color.Blue))
// 绘制一个40dp 的蓝色, background(Color.Blue).requiredSize(40.dp)这两个会放在一起,
// 因为遍历是从右往左的。这一行就会让DrawModifier和toWrap.entities.addBeforeLayoutModifier(toWrap, mod)
// requiredSize(40.dp) 这种最终都会生成一个ModifiedLayoutNode,主要是看 background 是和哪一个放在一起。
Box(Modifier.requiredSize(80.dp).background(Color.Blue).requiredSize(40.dp))

Box(
    Modifier.background(Color.Blue).background(Color.Blue).requiredSize(80.dp) // 放入一个新的ModifiedLayoutNode
        .background(Color.Green).background(Color.Red).requiredSize(40.dp) // 放入一个新的ModifiedLayoutNode
        .background(Color.Blue).background(Color.Blue) // 放入 InnerPlaceable
)


// 不清楚的请回顾:
ModifiedLayoutNode(// 检查这一层是否有设置 DrawModifier
    PaddingModifier,
    [DrawModifier2 -> DrawModifier1, null, null, null, null, null, null]
    ModifiedLayoutNode(// 检查这一层是否有设置 DrawModifier
        SizeModifier,
        [DrawModifier3, null, null, null, null, null, null]
        InnerPlaceable(// 有测量绘制功能, 这个是系统自带的,其余的都需要自己实现。
            [DrawModifier4 -> DrawModifier5 -> DrawModifier6, null, null, null, null, null, null]
        )
    )
)
关于canvas.saveLayer()详细补充

canvas.saveLayer()会在 GPU/CPU 上为当前的 Canvas 开辟一个离屏(off‑screen)缓存区,后续的所有绘制命令都会先绘到这个缓存区里;调用 restore()restoreToCount() 时,这个离屏缓存就会按之前设置的组合规则(比如带有 Xfermode 的 Paint)合并回原来的 Canvas。它是做蒙版、切图、透明度渐变、阴影等效果的必备工具。

典型例子:对图片做圆形蒙版:

kotlin 复制代码
// 假设有一个自定义 View,在它 的 onDraw(canvas: Canvas) 里:
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    val width = width.toFloat()
    val height = height.toFloat()
    val radius = min(width, height) / 2f

    // 1. 准备离屏 layer
    //    RectF 可以根据需要指定裁剪区域,这里用整个 View 大小
    val layerId = canvas.saveLayer(0f, 0f, width, height, null)

    // 2. 在离屏层先绘制原始内容 ------ 比如一张 Bitmap
    val bitmap = BitmapFactory.decodeResource(resources, R.drawable.your_image)
    canvas.drawBitmap(bitmap, null, RectF(0f, 0f, width, height), null)

    // 3. 设置蒙版------用 DST_IN 模式,只保留目的地(刚才的图片)和新绘图(圆形)重叠的部分
    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)

    // 4. 在离屏层上绘制一个圆
    canvas.drawCircle(width/2f, height/2f, radius, paint)

    // 5. 清除 Xfermode,恢复正常绘制
    paint.xfermode = null

    // 6. 将离屏 layer 合并回主 Canvas
    canvas.restoreToCount(layerId)
}

步骤解析:

  1. saveLayer()
    开辟一个离屏缓存,返回一个 layerId,之后所有绘制先入此缓存。
  2. 原始内容
    在离屏上先把你想处理的内容(Bitmap、文字、图形......)都绘制一遍。
  3. 设置 Xfermode
    使用 PorterDuffXfermode(Mode.DST_IN),它会保留"已有内容"与"圆形"重叠的部分。
  4. 绘制蒙版形状
    在离屏上画一个圆,完成对图片的圆形裁剪。
  5. restore() / restoreToCount()
    将离屏缓存按当前 Paint 的 Xfermode 规则合并回主 Canvas,并释放离屏。

这样,我们就得到了一个带圆形蒙版的图片效果。

原图:

裁剪之后的图:

上边只是一个示例,实际上,在 onDraw 利用 clipPath 或者是直接使用 ViewOutlineProvider 完全可以达到同样的效果:

kotlin 复制代码
class CircularImageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    private val path = Path()
    private var radius = 0f

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // 半径取短边的一半
        radius = min(w, h) / 2f
        path.reset()
        path.addCircle(w / 2f, h / 2f, radius, Path.Direction.CW)
    }

    override fun onDraw(canvas: Canvas) {
        // 裁剪画布到圆形区域
        canvas.save()
        canvas.clipPath(path)
        // 让 ImageView 正常绘制自己(会把 drawable 绘进去)
        super.onDraw(canvas)
        canvas.restore()
    }
}

class CircularImageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {

    init {
        // 设置轮廓:一个与 View 大小相同的椭圆
        outlineProvider = object : ViewOutlineProvider() {
            override fun getOutline(view: View, outline: Outline) {
                outline.setOval(0, 0, view.width, view.height)
            }
        }
        clipToOutline = true
    }
}

2. DrawModifier 的使用

通常开发者使用两种便捷的扩展函数来使用 DrawModifier 的能力:

2.1 Modifier.drawBehind
  • 功能:

    在内容绘制之前绘制自定义图形。它不会调用 drawContent(),因此只绘制我们的自定义内容,内容本身之后会被默认绘制。

  • 示例:

    kotlin 复制代码
    Modifier
        .drawBehind {
            // 绘制一个覆盖整个区域的蓝色矩形
            drawRect(color = Color.Blue, size = size)
        }

    在这个例子中,蓝色矩形被绘制在组件的背景上,接着组件内容会绘制在蓝色矩形之上。

2.2 Modifier.drawWithContent
  • 功能:

    允许我们在自定义绘制逻辑中显式调用默认内容绘制(drawContent()),从而可以在绘制前后加入自定义操作。

  • 示例:

    kotlin 复制代码
    Modifier
        .drawWithContent {
            // 在默认绘制之前可以绘制自定义内容
            drawRect(color = Color.LightGray, size = size)
    
            // 调用默认的绘制内容(例如 Text、Image 等)
            drawContent()
    
            // 在默认内容绘制之后再绘制一条红色对角线
            drawLine(
                color = Color.Red,
                start = Offset(0f, 0f),
                end = Offset(size.width, size.height),
                strokeWidth = 4f
            )
        }
    // 有一个简单的 Text 组件,在文本外添加一个自定义的装饰(例如一个圆形背景),但同时还要保留 Text 原本的文字内容.
    Text(
      text = "Hello World",
      modifier = Modifier.drawWithContent {
        // 自定义的绘制逻辑:首先绘制一个圆形背景
        drawCircle(
          color = Color.LightGray,
          radius = size.minDimension / 2,
          center = center
        )
        // 调用 drawContent(),绘制 Text 组件默认的内容,也就是 "Hello World" 文本
        drawContent()
      }
    )

    这样,我们可以先绘制背景、绘制组件的默认内容,然后在内容上方叠加绘制额外的装饰效果(例如红色对角线)。

2.3 自定义实现 DrawModifier

如果标准扩展函数无法满足需求,还可以自定义实现 DrawModifier 接口。例如:

kotlin 复制代码
class CustomBorderModifier(val borderWidth: Float, val borderColor: Color) : DrawModifier {
    override fun DrawScope.draw() {
        // 首先绘制子组件内容
        drawContent()
        // 然后绘制边框
        drawRect(
            color = borderColor,
            size = size,
            style = Stroke(width = borderWidth)
        )
    }
}

然后也可以直接这样使用:

kotlin 复制代码
Modifier.then(CustomBorderModifier(4f, Color.Green))

3. 总结

  • 核心作用:
    DrawModifier 允许我们在组件的绘制阶段添加、拦截或修改绘制逻辑,从而实现自定义的视觉效果。
  • 实现机制:
    通过在 Modifier 链中插入一个实现了 DrawModifier 的对象,Compose 在绘制时会依次调用各个 DrawModifier 的 draw() 方法。这些对象依赖于 DrawScope 提供的绘制 API,可以选择在调用 drawContent() 之前或之后执行自定义操作。
  • 常用扩展函数:
    Modifier.drawBehind 与 Modifier.drawWithContent 提供了便捷的方式,使大部分自定义绘制需求能够以声明式的方式实现,而无需重新构建整个绘制流程。

PointerInputModifier 的功能介绍和原理简析

PointerInputModifier 是负责拦截和处理原始触摸/鼠标等「指针事件」的底层机制。它并不是你直接拿来用的 API,而是通过一系列高级手势和交互(如 clickable { ... }draggable { ... }pointerInput { ... } 等)统一落到它头上来执行。下面分两部分:功能简介 & 原理简析。

Modifier 自带的点击事件的触发方式有两种:

kotlin 复制代码
Modifier.clickable {  } // 单击

fun Modifier.combinedClickable(
    enabled: Boolean = true,                      // 是否启用
    onClickLabel: String? = null,                 // 无障碍描述
    onLongClickLabel: String? = null,             // 无障碍描述
    onClick: () -> Unit,                          // 单击回调
    onLongClick: () -> Unit = {},                 // 长按回调
    onDoubleClick: () -> Unit = {},               // 双击回调
    role: Role? = Role.Button,                    // Accessibility Role
    indication: Indication? = rememberRipple(),   // 点击时的水波纹效果
    interactionSource: MutableInteractionSource    // 用于跟踪按下、释放、焦点等交互状态
)

// 使用:

@Composable
fun CombinedClickableSample() {
    // 用于指示器(Ripple)的状态追踪
    val interactionSource = remember { MutableInteractionSource() }

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.LightGray)
            .combinedClickable(
                enabled = true,
                onClickLabel = "点击",
                onLongClickLabel = "长按",
                onClick = { Log.d("Demo", "单击了") },
                onLongClick = { Log.d("Demo", "长按了") },
                onDoubleClick = { Log.d("Demo", "双击了") },
                role = Role.Button,
                indication = rememberRipple(bounded = true), 
                interactionSource = interactionSource
            ),
        contentAlignment = Alignment.Center
    ) {
        Text("点我", color = Color.Black)
    }
}

combinedClickableclickable 的区别:

  • clickable 只支持单击;如果同时添加多个手势(如拖拽、滑动)可能会冲突;
  • combinedClickable 同时集成了长按和双击检测,且内部已经做了手势冲突解决,更加健壮。

一、功能简介

  1. 捕获原始指针事件

    • PointerInputModifier 能拦截最底层的触摸事件(MotionEvent)、鼠标事件,甚至滚轮、手写笔等,统一转换成 Compose 内部的 PointerInputEvent
  2. 支持自定义手势

    • 你可以在 Modifier 链上调用

      kotlin 复制代码
      Modifier.pointerInput(key1, key2, ...) { // 类似于自带GestureDetector的onTouchEvent
        // 在协程里 awaitPointerEventScope { ... } / detectTapGestures { ... }  // 监听与点击有关的事件
      }
      // 还可以这样使用:
      Modifier.pointerInput(Unit) {// 监听与点击有关的
        ,detectTapGestures(
            onTap = {}, 
            onDoubleTap = {},
            onLongPress = {},
            onPress = {}) // 摸到屏幕的监听
      
        forEachGesture { // 循环侦测,一般要用,否则只能侦测一次,detectTapGestures 等一系列都是这样实现的。
          awaitPointerEventScope { // 监听每一个触摸事件,例如按下抬起。
            val down = awaitFirstDown()
            // 监听抬起
          }
        }
      }

      Modifier.pointerInput(Unit) 与 combinedClickable 有什么区别呢?combinedClickable 的内部其实也是使用 detectTapGestures 来实现的,安卓和Compose 都支持鼠标,鼠标的点击事件不会触发 onTap ,但是会触发 onClick,另外 combinedClickable 也是没有这个回调的,但是内部其实是利用了这个回调。

上边写到的代码逻辑,这其实会在背后添加一个 PointerInputModifier,启动一个协程来不断接收、处理和消费事件。PointerInputModifier 一共有两个实现类,一个是SuspendingPointerInputFilter, 另一个是 PointerInteropFilter,后者是用来和android中传统的 View 系统交互的, 我们填写的 PointerInputModifier 去哪里呢?其实和之前的 DrawModifier 一样,也是在androidx.compose.ui.node.LayoutNode#modifier 中处理的,因此可以知道,PointerInputModifier 也是对挨着它最近的右侧 LayoutModifier 生效的。

kotlin 复制代码
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
    if (mod is RemeasurementModifier) {
        mod.onRemeasurementAvailable(this)
    }

    toWrap.entities.addBeforeLayoutModifier(toWrap, mod) // 在这里边处理的。

    if (mod is OnGloballyPositionedModifier) {
        getOrCreateOnPositionedCallbacks() += toWrap to mod
    }

下边这个代码是如何生效的?

kotlin 复制代码
Modifier.pointerInput().pointerInput().size(??) // 左侧的pointerInput是右侧pointerInput的父点击

hitTest就是事件处理的核心机制,

kotlin 复制代码
internal fun hitTest(
    pointerPosition: Offset,
    hitTestResult: HitTestResult<PointerInputFilter>,
    isTouchEvent: Boolean = false,
    isInLayer: Boolean = true
) {
    val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
    outerLayoutNodeWrapper.hitTest(
        LayoutNodeWrapper.PointerInputSource,
        positionInWrapped,
        hitTestResult,
        isTouchEvent,
        isInLayer
    )
}

fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> hitTest(
    hitTestSource: HitTestSource<T, C, M>,
    pointerPosition: Offset,
    hitTestResult: HitTestResult<C>,
    isTouchEvent: Boolean,
    isInLayer: Boolean
) {
    val head = entities.head(hitTestSource.entityType()) // 为什么不直接通过Index去取,而是使用hitTestSource.entityType(),因为hitTest会被两种Modifier都会去使用的,处理 PointerInputModifier 还有 `SemanticsModifier(使用无障碍功能)` 也会使用。
    // 省略其余代码......
        head.hit(
            hitTestSource,
            pointerPosition,
            hitTestResult,
            isTouchEvent,
            isInLayer
        )
    // 省略其余代码......
    }
}

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) { // hitTestSource.contentFrom(this来源于下边的 contentFrom

            next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer) // 递归调用。
        }
    }
}

val PointerInputSource =
    object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
        override fun entityType() = EntityList.PointerInputEntityType

        @Suppress("ModifierFactoryReturnType", "ModifierFactoryExtensionFunction")
        override fun contentFrom(entity: PointerInputEntity) = 
            entity.modifier.pointerInputFilter //  entity.modifier 链表中的头节点

        override fun interceptOutOfBoundsChildEvents(entity: PointerInputEntity) =
            entity.modifier.pointerInputFilter.interceptOutOfBoundsChildEvents

        override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) = true

        override fun childHitTest(
            layoutNode: LayoutNode,
            pointerPosition: Offset,
            hitTestResult: HitTestResult<PointerInputFilter>,
            isTouchEvent: Boolean,
            isInLayer: Boolean
        ) = layoutNode.hitTest(pointerPosition, hitTestResult, isTouchEvent, isInLayer)
    }
  1. 手势冲突 & 事件消费

    • 多个 PointerInputModifier(或更高层的 clickabledraggable)同时存在时,Compose 会按布局树深度与声明顺序,把事件分发到各个 PointerInputModifier,支持「先消费,后阻止向下/向上传递」的逻辑,从而处理好手势冲突。
  2. 与 Compose 渲染解耦

    • 它只关心「事件」,不参与布局测量和绘制,确保手势处理不会阻塞 UI 树的重排或重绘。

二、原理简析

  1. Modifier 树中插入 PointerInputModifierNode

    • 当你调用 Modifier.pointerInput { ... },Compose 会在该节点的 Modifier 列表中插入一个 PointerInputModifierNode,并在布局阶段把它装配到对应的 LayoutNode 里的 pointerInputFilters 列表。
  2. PointerInputDispatcher & Dispatcher Loop

    • 每个 Owner(通常是最顶层的 ComposeView)维护一个 PointerInputDispatcher,它订阅 Android 系统的原始触摸回调(onTouchEvent(MotionEvent))。
    • 系统一旦有触摸事件到来,ComposeView 会把它封装成 PointerInputEvent,交给 dispatcher.dispatch(event)
  3. 事件分发

    • PointerInputDispatcher 会遍历所有注册在各个 LayoutNode 上的 PointerInputModifierNode(按从最里到最外、从深到浅的顺序),依次调用它们的 onPointerEvent()
    • 每个 PointerInputModifierNode 内部会把事件发给自身启动的协程(pointerInput block)里的 awaitPointerEventScope,或调用 pointerInputFilter.pointerInputFilter() 方法。
  4. 协程 & Await 系统

    • pointerInput { ... } 本质上是一个被 LaunchedEffect 管理的协程,它在一个 forEachGestureawaitPointerEventScope 循环中挂起和恢复:

      kotlin 复制代码
      pointerInput(key) {
        awaitPointerEventScope {
          while (true) {
            val event = awaitPointerEvent()
            // 对 event.positions 做判定、消费 (event.changes.consume())
          }
        }
      }
    • onPointerEvent 到来时,会 resume 相应协程,把新事件交给你。

  5. 事件消费 & 迭代

    • 你可以在处理完某些指针变化后,调用 change.consume()(或更细粒度的 consumePositionChange()consumeDownChange())来告诉框架「这个事件部分已经被我消费,不要再分发给更上层或其他 handler」。
  6. 生命周期管理

    • 当对应的 Composable 或 Modifier 离开 Composition,后台会自动取消那个协程并移除 PointerInputModifierNodePointerInputDispatcher 也就不会再给它分发事件,保证不会泄漏。

总结

  • PointerInputModifier = Compose 最底层的「指针事件过滤器」,所有手势 API 都是建立在它之上。
  • 事件流 :Android MotionEventPointerInputEventPointerInputDispatcher → 各 PointerInputModifierNode.onPointerEvent() → 你的 pointerInput { ... } 协程。
  • 它的核心优势在于:基于协程的等待/恢复模型可消费/可拦截与布局/绘制解耦,从而让手势逻辑既灵活又高效。

ParentDataModifier

ParentDataModifier 是一种特殊的 Modifier,它不直接参与子组件的测量或绘制,而是用来将"父布局需要的额外数据"从子组件"上传"到父布局。设置在子组件,但是却是给父组件用的,在布局过程中用于辅助测量的。在经典的 Android View 世界里,这个作用就相当于 LayoutParams(比如 LinearLayout.LayoutParams 中的 weightgravity 等属性);在 Compose 里,就是 ParentDataModifierweightgravity 这些属性用 LayoutModifier 是实现不了的, 因为LayoutModifier只是关注自己的实现。


一、为什么需要 ParentDataModifier

很多布局组件(如 RowColumnBox)在摆放子项时,需要额外的"子项元数据"来决定如何测量或定位它们:

  • RowColumn 里的 weight

  • Box 里的 align 或者 bias

  • 自定义布局里某些子项可能需要提供"占位优先级"或"某个方向上的偏移量"

  • Modifier.layoutId("") 与 传统View 中的 ID 不同,只用用来做辅助测量的,例如下边的自定义 Compose 组件的示例,通过不同的ID触发不同的布局方式。
    *

    kotlin 复制代码
    CustomLayout(Modifier.size(40.dp)) {
        Text(text = "rengwuxian", Modifier.layoutId("big"))
        Text(text = "扔物线", Modifier.layoutId("small"))
        Box(Modifier.size(20.dp).background(Color.Red))
    }
    
    @Composable
    fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
        Layout(content, modifier) { measurables, constraints ->
            measurables.forEach { it: Measurable
                when (it.layoutId) {
                    "big" -> it.measure(constraints.xxxx)
                    "small" -> it.measure(constraints.yyyy)
                    else -> it.measure(constraints)
                }
            }
        }
    }
  • Layout(content = , measurePolicy = ) 用来在最底层做最完整的测量。这是所有 Compose 函数最底层的实现。

这些元数据并不属于子组件本身的绘制或测量逻辑,而是由父布局在布局阶段去读取并据此调整位置/尺寸。ParentDataModifier 就是实现这一传输的桥梁。


二、ParentDataModifier 的写法

写法
  • 前提:首先是要用 Layout() 组件去写一个自定义布局,才会用得到它;

  • 自定义布局的测量和布局的算法里,用 Measurable.parentData 拿到开发者给子组件设置的属性,然后去计算,Measurable.parentData 是用来对多个不同属性进行融合的,而不是对同一个属性做融合。

  • 写一个自定义的 Modifier 函数,让它的内部创建一个 ParentDataModifier,并且实现它的 modifyParentData() 函数,在里面提供对应的 parentData

    • modifyParentData() 的函数参数,是下一个 ParentDataModifier 所提供的数据,可以和自己的数据进行融合后提供。
    • 可以把 Modifier 函数写专门的接口或 object 对象,来避免 API 污染的问题(参照Row的写法)。
kotlin 复制代码
// 使用示例:在 CustomLayout2 中,你可以为子元素添加权重(weightData)和"大"标记(bigData)
CustomLayout2 {
    // 下面这行,编译器知道这是处在 CustomLayout2Scope 的接收者范围内,
    // 可以无冲突地调用 weightData()、bigData()。
    Text(
        "1",
        Modifier
            .weightData(1f)   // 为这个子项设置权重
            .bigData(true)    // 标记为"大"布局
    )
    // 第二个 Text:没有额外的 parentData
    Text("2")
    // 第三个子元素在一个 Box 中,也可以使用 weightData
    Box {
        Text("3", Modifier.weightData(1f)) // 报错,weightData 没有直接在 CustomLayout2 中使用,而是在 CustomLayout2 下的 Box 中使用。
    }
}

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(content, modifier) { measurables, constraints ->
        // 遍历每个可测量项,根据它的 layoutId 决定测量方式
        measurables.forEach { measurable ->
            when (measurable.layoutId) {
                "big" -> measurable.measure(constraints.xxxx)    // "big" 专用约束
                "small" -> measurable.measure(constraints.yyyy)  // "small" 专用约束
                else -> measurable.measure(constraints)          // 默认约束
            }
        }
        // 最终布局大小与放置逻辑
        layout(width = 100, height = 100) {
            // 在这里调用 place() 将子元素放入具体位置
            ...
        }
    }
}

@Composable
fun CustomLayout2(
    modifier: Modifier = Modifier,
    content: @Composable CustomLayout2Scope.() -> Unit
) {
    // 构造一个受 CustomLayout2Scope 扩展的 content
    Layout(
        content = { CustomLayout2Scope.content() },
        modifier = modifier
    ) { measurables, constraints ->

        // ------ 第一步:遍历所有 measurables
        measurables.forEach { measurable ->
            // ------ 第二步:从 measurable.parentData 中取出前面设置的 Layout2Data
            val data = measurable.parentData as? Layout2Data
            // 然后就可以读取出两个自定义属性
            val big = data?.big           // 是否标记为"大"
            val weight = data?.weight     // 权重值
            // 根据 big、weight 进行测量逻辑
            ...
        }

        // 最终布局
        layout(width = 100, height = 100) {
            ...
        }
    }
}

// 用来承载综合数据的类,可以同时使用多个自定义的属性。
class Layout2Data(var weight: Float = 0f, var big: Boolean = false)

@LayoutScopeMarker // 这个扩展函数只在 `CustomLayout2Scope` 内可用, 且必须是直接在里边,而不能是间接的。
object CustomLayout2Scope {
    /**
     * 为当前元素添加一个 weightData(parentData),
     * 该 parentData 会被 CustomLayout2 在测量阶段读取。
     */
    fun Modifier.weightData(weight: Float) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any? {
            // ------ 第三步:如果已有 parentData,就更新 weight,否则创建新的 Layout2Data
            return if (parentData == null) {
                Layout2Data(weight = weight)
            } else {
                (parentData as Layout2Data).apply {// 参数融合,Modifier.weightData(1f).bigData(true)),因为默认的 bigData 为 false

                    this.weight = weight
                }
            }
        }
    })

    /**
     * 为当前元素添加一个 bigData(parentData),
     * 标记它是否是"大"元素。
     */
    fun Modifier.bigData(big: Boolean) = then(object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any? {
            // 如果已有 Layout2Data,就更新 big;否则新建一个
            return ((parentData as? Layout2Data) ?: Layout2Data()).also {// 参数融合,Modifier.weightData(1f).bigData(true)),因为默认的 weight 为 0f

                it.big = big
            }
        }
    })
}
Row/Column 的 weight
kotlin 复制代码
Row(modifier = Modifier.fillMaxWidth()) {
    Box(
        modifier = Modifier
            .weight(1f) // 这里其实就是添加了一个 ParentDataModifier
            .height(50.dp)
            .background(Color.Red)
    )
    Box(
        modifier = Modifier
            .weight(2f)
            .height(50.dp)
            .background(Color.Blue)
    )
}
  • Modifier.weight(...) 底层插入了一个 ParentDataModifier,把 weight 值"存"在子节点上。
  • Row 在测量时,会遍历它所有子节点,读取每个 child 的 weight(通过 child.parentData as? WeightParentData),然后根据权重分配可用宽度。
Box 的 align
kotlin 复制代码
Box(modifier = Modifier.size(200.dp)) {
    Text(
        "Bottom End",
        modifier = Modifier
            .align(Alignment.BottomEnd)  // 添加了 ParentDataModifier,告诉 Box 在右下角绘制
    )
}
  • Modifier.align() 也是个 ParentDataModifier,它把对齐信息(Alignment)附加给子布局。
  • Box 在布局时,就会检查每个子项的 parentData(BoxParentData),据此决定子项的 x,y 坐标。

三、原理简析

  1. 插入节点

    当你在 Modifier 链里调用如 weight()align() 等 API 时,Compose 会往这个子节点的 Modifier 列表中插入一个实现了 ParentDataModifier 接口的节点。

  2. 数据携带

    这个 Modifier 内部会保存一份针对当前子节点的"父数据"(例如 weight = 1f),并重写 modifyParentData 方法返回一个相应的 ParentData 对象。

  3. 父布局读取

    在父布局的 MeasurePolicy 里(measurelayout 阶段),它会调用类似 child.parentData(或在 DSL 中通过 data = childData())来获取 ParentData

    • Compose 会自动把子节点的所有 ParentDataModifier 累积起来,形成最终的 LayoutNode.parentData
  4. 依据父数据布局

    父布局根据读取到的 parentData 来调整测量规则、分配空间、或者定位坐标。


四、自定义 ParentDataModifier

如果在写自定义布局,也可以定义自己的 ParentDataModifier

kotlin 复制代码
// 1. 定义存储的数据类型
data class MyParentData(val priority: Int)

// 2. 定义 Modifier
fun Modifier.priority(value: Int) = this.then(
    object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any {
            // parentData 可能是上一个 ParentDataModifier 的结果
            return (parentData as? MyParentData)?.copy(priority = value)
                ?: MyParentData(value)
        }
    }
)

// 3. 自定义布局读取
@Composable
fun MyCustomLayout(content: @Composable () -> Unit) {
    Layout(content) { measurables, constraints ->
        // 先测量并获得每个子项的 parentData
        val placeablesWithData = measurables.map { measurable ->
            val placeable = measurable.measure(constraints)
            val pd = measurable.parentData as? MyParentData ?: MyParentData(0)
            placeable to pd.priority
        }
        // 按 priority 排序再布局
        val sorted = placeablesWithData.sortedByDescending { it.second }
        layout(constraints.maxWidth, constraints.maxHeight) {
            var x = 0
            sorted.forEach { (placeable, priority) ->
                placeable.placeRelative(x, 0)
                x += placeable.width
            }
        }
    }
}

这样,我们就通过 Modifier.priority(...) 把一个自定义的"优先级"上传给父布局,并在布局阶段读取并使用它。


ParentDataModifier 的原理

在测量与布局时候起作用,且与右边最近的 LayoutModifier 存在一起,被放进了装入链表的数组:

kotlin 复制代码
Modifier.then(ParentDataModifier1).then(ParentDataModifier2).then(LayoutModifier1)
    .then(ParentDataModifier3).then(ParentDataModifier4).then(LayoutModifier2)
    .then(ParentDataModifier5).then(ParentDataModifier6)

// 上边的代码对应的结构:
ModifiedLayoutNode(
    LayoutModifier1,
    [
        ...
        ParentDataModifier1 -> ParentDataModifier2,
        ...
    ],
    ModifiedLayoutNode(
        LayoutModifier2,
        [
            ...
            ParentDataModifier3 -> ParentDataModifier4,
            ...
        ],
        InnerPlaceable(
            [
                ...
                ParentDataModifier5 -> ParentDataModifier6
                ...
            ]
        )
    )
)

// 完整示例:
@Composable
@Preview
fun ParentDataModifierChainExample() {
  // 定义 6 个 ParentDataModifier,分别返回不同的标记
  val parentDataModifier1 = object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? = "PD1"
  }
  val parentDataModifier2 = object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? = "PD2"
  }
  val parentDataModifier3 = object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? = "PD3"
  }
  val parentDataModifier4 = object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? = "PD4"
  }
  val parentDataModifier5 = object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? = "PD5"
  }
  val parentDataModifier6 = object : ParentDataModifier {
    override fun Density.modifyParentData(parentData: Any?): Any? = "PD6"
  }

  // 第一个 LayoutModifier(ModifiedLayoutNode)
  val layoutModifier1 = object : LayoutModifier {
    override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
      // 这里 measurabe.parentData 会是 PD1、PD2 串联的结果
      Log.d("zxc LM1", "parentData = ${measurable.parentData}")  // → "PD3"
      val placeable = measurable.measure(constraints)
      return layout(placeable.width, placeable.height) {
        placeable.place(0, 0)
      }
    }
  }

  // 第二个 LayoutModifier(Nested ModifiedLayoutNode)
  val layoutModifier2 = object : LayoutModifier {
    override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
      // 这里 measurable.parentData 会是 PD3、PD4 串联的结果
      Log.d("zxc LM2", "parentData = ${measurable.parentData}")  // → "PD5"
      val placeable = measurable.measure(constraints)
      return layout(placeable.width, placeable.height) {
        placeable.place(0, 0)
      }
    }
  }

  Box(
    Modifier
      // → ModifiedLayoutNode(LM1) 拥有 [PDM1, PDM2]
      .then(parentDataModifier1)
      .then(parentDataModifier2)
      .then(layoutModifier1)
      // → ModifiedLayoutNode(LM2) 拥有 [PDM3, PDM4]
      .then(parentDataModifier3)
      .then(parentDataModifier4)
      .then(layoutModifier2)
      // → InnerPlaceable 拥有 [PDM5, PDM6]
      .then(parentDataModifier5)
      .then(parentDataModifier6)
  ) {
    Text("看 Logcat")
  }
}
// 结果:
LM1                 com...n.coursecomposemodifierlayout  D  parentData = PD3
LM2                 com...n.coursecomposemodifierlayout  D  parentData = PD5
源码分析

获取数据的起源就在 parentData 中

kotlin 复制代码
measurables.forEach { measurable ->
    // ------ 第二步:从 measurable.parentData 中取出前面设置的 Layout2Data
    val data = measurable.parentData as? Layout2Data
    // 然后就可以读取出两个自定义属性
    val big = data?.big           // 是否标记为"大"
    val weight = data?.weight     // 权重值
    // 根据 big、weight 进行测量逻辑
    ...
}

一切的起源来自:androidx.compose.ui.node.LayoutNodeWrapper#parentData

kotlin 复制代码
override val parentData: Any?
    // 拿到装有 parentData 的链表的表头节点,然后获取 parentData 
    get() = entities.head(EntityList.ParentDataEntityType).parentData

// SimpleEntity 的一个拓展属性就是 parentData
private val SimpleEntity<ParentDataModifier>?.parentData: Any?
    get() = if (this == null) { // 表示这个 ModifiedLayoutNode 的 ParentDataModifier 链表为空
        wrapped?.parentData // 返回它的内部的 ModifiedLayoutNode 的 ParentDataModifier ,这个就是一次递归。
    } else {
        with(modifier) { // 头节点不为空,
            /**
             * ParentData provided through the parentData node will override the data provided
             * through a modifier.
             */
            measureScope.modifyParentData(next.parentData) // 这里就是执行自定义的函数的位置, 比如下边代码的 modifyParentData 。
            // next.parentData 表示链表中的下一个节点。又是一次递归。
        }
    }

/* fun Modifier.priority(value: Int) = this.then(
    object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any {
            // parentData 可能是上一个 ParentDataModifier 的结果
            return (parentData as? MyParentData)?.copy(priority = value)
                ?: MyParentData(value)
        }
    }
) */

对于这个例子:

kotlin 复制代码
Modifier.then(ParentDataModifier1).then(ParentDataModifier2).then(LayoutModifier1)
    .then(ParentDataModifier3).then(ParentDataModifier4).then(LayoutModifier2)
    .then(ParentDataModifier5).then(ParentDataModifier6)

得出的他最终的遍历顺序就是:

kotlin 复制代码
ModifiedLayoutNode
  → ParentDataModifier1
  → ParentDataModifier2
  → ModifiedLayoutNode
     → ParentDataModifier3
     → ParentDataModifier4
     → InnerPlaceable
        → ParentDataModifier5
        → ParentDataModifier6

因此,即便顺序调整之后,最终的显示效果还是一样的,也正因为是提供个父组件用的,所以下边这种写法是没有效果的:

kotlin 复制代码
Box(
  Modifier
    .size(40.dp)
    .background(Color.Green)
    .padding(20.dp)
    .layout { measurable, constraints -> // 无效
      measurable.parentData
    })

这是因为: Modifier.layout { ... } 并不是在写一个「自定义布局」Composable,它只是给当前这个 单独元素 (这里是 Box)挂了一个 LayoutModifier,它的 measure 回调里:

  1. 只能拿到这个元素自己measurable 就代表 Box 本身)的 parentData,而不是你想取的 Box 里子组件parentData

  2. 它的回调签名是

kotlin 复制代码
MeasureScope.(measurable: Measurable, constraints: Constraints) -> MeasureResult

必须在里面调用

kotlin 复制代码
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
  placeable.place(0, 0)
}

否则整个布局根本就不会走测量/放置流程,等同于"空操作"。

小结

  • 作用:在子组件的 Modifier 上携带"父布局需要的元数据",供父布局在测量/布局阶段读取使用。
  • 本质 :Compose 里的 "布局参数" ------ 就像 View 中的 LayoutParams,但更灵活可组合。
  • 使用方式 :内置的 weightalignbaselinePaddinggravity 等;也可以自定义 ParentDataModifier 搭配自定义布局。

SemanticsModifier 的作用、写法和原理

SemanticsModifier 是 Compose 中用来为 UI 元素提供可访问性(Accessibility)以及测试标签信息的关键手段。它会在布局树中生成或修改对应的 SemanticsNode,把**语义(role、state、action、contentDescription、testTag 等)**暴露给无障碍服务(TalkBack)、UI 测试框架或其它需要"看懂"界面结构的工具。


写法

1. 使用 semantics 或高阶 API
  • 最常见 :用 Modifier.semantics { ... }

    kotlin 复制代码
    Box(Modifier
      .size(100.dp)
      .semantics(/*true*/) { // 改为true之后,会与子组件进行合并,同时让自己不会被合并到外部去。
        contentDescription = "用户头像"
        role = Role.Image
        stateDescription = if (selected) "已选中" else "未选中"
        onClick { /* 点击行为 */ true }
      }
    )
  • 专用便捷 API

    Compose 也提供一些常见语义修饰符,如:

    kotlin 复制代码
    Modifier.clickable { ... }           // 自动添加 onClick、role=Button
    Modifier.toggleable(checked) { ... } // 添加 onClick, role=Switch, stateDescription
    Modifier.clearAndSetSemantics {    // 清空下层语义,设置全新语义,绝大多数时候都不需要设置。
      testTag = "LoginButton"
    }
2. 自定义 SemanticsModifier

如果需要更细粒度控制,可以直接实现 SemanticsModifier 接口,本质上它继承自 Modifier.Element

kotlin 复制代码
// 1) 定义一个 PropertyKey
val CountKey = SemanticsPropertyKey<Int>("Count")
// 2) 给 SemanticsConfiguration 的接收者增加一个委托属性
var SemanticsPropertyReceiver.count by CountKey

// 3) 写个 Modifier 扩展,用官方的 semantics{} DSL
fun Modifier.countSemantics(count: Int): Modifier =
    this.then(
        semantics {
            this.count = count
        }
    )
// 4) 使用示例
@Composable
fun Demo() {
    Box(
        Modifier
            .size(80.dp)
            .countSemantics(42)  // 无障碍和测试就能读到 CountKey = 42
    ) {
        Text("Tap me")
    }
}

然后在 Modifier 链中使用:

kotlin 复制代码
Box(Modifier.then(MySemanticsModifier("自定义标签")))

原理

kotlin 复制代码
val PointerInputSource =
    object : HitTestSource<PointerInputEntity, PointerInputFilter, PointerInputModifier> {
        override fun entityType() = EntityList.PointerInputEntityType

        @Suppress("ModifierFactoryReturnType", "ModifierFactoryExtensionFunction")
        override fun contentFrom(entity: PointerInputEntity) =
            entity.modifier.pointerInputFilter

        override fun interceptOutOfBoundsChildEvents(entity: PointerInputEntity) =
            entity.modifier.pointerInputFilter.interceptOutOfBoundsChildEvents

        override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) = true

        override fun childHitTest(
            layoutNode: LayoutNode,
            pointerPosition: Offset,
            hitTestResult: HitTestResult<PointerInputFilter>,
            isTouchEvent: Boolean,
            isInLayer: Boolean
        ) = layoutNode.hitTest(pointerPosition, hitTestResult, isTouchEvent, isInLayer)
    }

/**
 * Hit testing specifics for semantics.
 */
val SemanticsSource =
    object : HitTestSource<SemanticsEntity, SemanticsEntity, SemanticsModifier> {
        override fun entityType() = EntityList.SemanticsEntityType

        override fun contentFrom(entity: SemanticsEntity) = entity

        override fun interceptOutOfBoundsChildEvents(entity: SemanticsEntity) = false

        override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) =
            parentLayoutNode.outerSemantics?.collapsedSemanticsConfiguration()
                ?.isClearingSemantics != true

        override fun childHitTest(
            layoutNode: LayoutNode,
            pointerPosition: Offset,
            hitTestResult: HitTestResult<SemanticsEntity>,
            isTouchEvent: Boolean,
            isInLayer: Boolean
        ) = layoutNode.hitTestSemantics(
            pointerPosition,
            hitTestResult,
            isTouchEvent,
            isInLayer
        )
    }

internal fun hitTest(
    pointerPosition: Offset,
    hitTestResult: HitTestResult<PointerInputFilter>,
    isTouchEvent: Boolean = false,
    isInLayer: Boolean = true
) {
    val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
    outerLayoutNodeWrapper.hitTest(
        LayoutNodeWrapper.PointerInputSource,
        positionInWrapped,
        hitTestResult,
        isTouchEvent,
        isInLayer
    )
}

@Suppress("UNUSED_PARAMETER")
internal fun hitTestSemantics( // 判断触摸的视图点在哪里。
    pointerPosition: Offset,
    hitSemanticsEntities: HitTestResult<SemanticsEntity>,
    isTouchEvent: Boolean = true,
    isInLayer: Boolean = true
) {
    val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
    outerLayoutNodeWrapper.hitTest(
        LayoutNodeWrapper.SemanticsSource,
        positionInWrapped,
        hitSemanticsEntities,
        isTouchEvent = true,
        isInLayer = isInLayer
    )
}
  1. 语义节点树构建

    • Compose 在布局测量与放置阶段,会同时收集所有 SemanticsModifier(及其它产生语义的 Modifier,如 clickabletoggleable
    • 它们会被包装成一个或多个 SemanticsNode,构成一棵平行于布局树的"语义树"(AccessibilityNodeInfo 在底层映射自这里)。
  2. SemanticsConfiguration 聚合

    • 每个 SemanticsModifier 都提供一个 SemanticsConfiguration,里面是键值对形式的语义属性。
    • 当多个修饰符应用在同一个 UI 节点上,系统会 合并 它们的配置:同一 key 后面的会覆盖前面的,action 列表会累加。
  3. 无障碍桥接

    • 最终框架会把这棵语义树交给 Android Accessibility Framework(AccessibilityNodeInfo),或给测试库(Espresso Compose)解析。
    • TalkBack、UI Automator 等工具就可以读取 contentDescriptionrolestateDescriptiontestTag,甚至触发 onClickcustomActions 等。
  4. 优化与合并策略

    • clearAndSetSemantics { ... } :可清空所有下层语义,防止父组件意外继承子组件的无障碍信息。
    • 合并策略 :如果一个容器和子项都定义了语义,默认会向上合并,生成整体的可访问性焦点区域。

小结

  • 为什么要用:让 Compose UI 对无障碍读屏和自动化测试友好------提供文字描述、角色信息、状态、操作回调。
  • 怎么用 :最常见是 Modifier.semantics { ... } 或者使用框架提供的 clickabletoggleabletestTag 等便捷语义 Modifier。
  • 底层原理 :Compose 会把所有 SemanticsModifier 收集进平行的"语义树",并在渲染时转换成 Android AccessibilityNodeInfo,创建真正的无障碍节点。

addBeforeLayoutModifier-() 和 addAfterLayoutModifier-() 的区别

惟一的区别就是:对于同一个Modifier,当它有多重身份的时候,应该先处理它的哪个身份?例如下边这个例子,这也是目前唯一具有双重身份的Modifier:

kotlin 复制代码
private class PainterModifier(
    val painter: Painter,
    val sizeToIntrinsics: Boolean,
    val alignment: Alignment = Alignment.Center,
    val contentScale: ContentScale = ContentScale.Inside,
    val alpha: Float = DefaultAlpha,
    val colorFilter: ColorFilter? = null,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, DrawModifier, InspectorValueInfo(inspectorInfo) {

但是实际上,

  • addBeforeLayoutModifier: addBefore-() 会让⾥⾯的 Modifier 身份早于 LayoutModifier 身份被处理,导致被包含在 LayoutModifier 所处的 ModifiedLayoutNode 的更内部的⼀层,从⽽受到更内部的layoutNodeWrapper 的尺⼨限制。

  • addAfterLayoutModifier:addAfter-() 会让⾥⾯的 Modifier 身份在 LayoutModifier 身份之后被处理,这样就和 LayoutModifier 处于同⼀个 ModifiedLayoutNode ,从⽽和 LayoutModifier 的尺⼨范围是⼀样的。

kotlin 复制代码
Modifier.then(parentDataModifier1).then(parentDataModifier2).then(onRemeasuredModifier1).then(onRemeasuredModifier2).then(layoutModifier1)
.then(parentDataModifier3).then(parentDataModifier4).then(onRemeasuredModifier3).then(onRemeasuredModifier4).then(layoutModifier2)
.then(parentDataModifier5).then(parentDataModifier6).then(onRemeasuredModifier5)

// 得到的结果:
ModifiedLayoutNode(
    LayoutModifier1,
    [
        ParentDataModifier1 -> ParentDataModifier2,
        onRemeasuredModifier1 -> onRemeasuredModifier2
    ],
    ModifiedLayoutNode(
        LayoutModifier2,
        [
            ParentDataModifier3 -> ParentDataModifier4,
            onRemeasuredModifier3 -> onRemeasuredModifier4
        ],
        InnerPlaceable(
            [
                ParentDataModifier5 -> ParentDataModifier6,
                onRemeasuredModifier5
            ]
        )
    )
)

OnRemeasuredModifier 的作用、写法和原理

作用

OnRemeasuredModifier 对应传统 View 的 onMeasure(),当它修饰的 最近右侧 那个 LayoutModifier(或最内层内容)完成测量后就会回调一次 onRemeasured(size: IntSize),让我们拿到测量出来的宽高信息,以便做接下来的逻辑(比如驱动动画、收集日志或更新状态)。

写法

  1. 直接实现

    kotlin 复制代码
    Text(
      "Hello",
      Modifier
        .padding(20.dp)
        .then(object : OnRemeasuredModifier {
          override fun onRemeasured(size: IntSize) {
            // size 就是右侧 padding(...) 或者紧挨它的 LayoutModifier 测量后的最终尺寸
            Log.d("Demo", "Measured size: $size")
          }
        })
        .padding(40.dp)
    )

    在上面例子里,onRemeasured 会回调 40.dp 这个 padding 测量后的大小,因为它总是与它右侧最近的那个 LayoutModifier 关联。​CSDN博客

  2. 内置封装 onSizeChanged

    kotlin 复制代码
    Text(
      "World",
      Modifier.onSizeChanged { newSize ->
        // 只有当尺寸发生"变化"时才回调一次
        println("Size changed to $newSize")
      }
    )

    Modifier.onSizeChanged { ... } 底层就是一个 OnSizeChangedModifier,它实现了 OnRemeasuredModifier,并且会在每次测量完成后,只有当 newSize != previousSize 时才真正触发。

原理

kotlin 复制代码
internal val nodes = NodeChain(this)
internal val innerCoordinator: NodeCoordinator // 对应旧版本的InnerPlaceable
    get() = nodes.innerCoordinator
internal val layoutDelegate = LayoutNodeLayoutDelegate(this)
internal val outerCoordinator: NodeCoordinator // 对应旧版本的outWrapper,最外层的装着 LayoutModifier 的 用于测量的 LayoutNodeWrapper,它便是最外层的,对应着最左边的 LayoutModifier 的 NodeCoordinator。 在新版本的 Modifier 统一都被放在了 nodes 里边了 ,而不是  放在 NodeCoordinator 的里边了,有了这种存储关系的调整,但是对应关系还是有的。一个 Node 还是对应一个 NodeCoordinator ,NodeCoordinator 依然是布局进行分层的工具。里边会关联着 LayoutModifier 

NodeChain 是用来存放Modifier 的, 而且 这些 Modifier 会被封装进 一个个 Node 对象里边 ,这些 Node存放在哪里呢?就是 head 和 tail ,分别是双向链表的头节点和尾结点。

kotlin 复制代码
private fun syncCoordinators() {
    var coordinator: NodeCoordinator = innerCoordinator
    var node: Modifier.Node? = tail.parent
    while (node != null) {
        if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
            val next = if (node.isAttached) {
                val c = node.coordinator as LayoutModifierNodeCoordinator
                val prevNode = c.layoutModifierNode
                c.layoutModifierNode = node
                if (prevNode !== node) c.onLayoutModifierNodeChanged()
                c
            } else {
                // 处理多层嵌套,与旧版本一样,处理 LayoutModifier 都需要创建 ModifierLayoutModifier 包裹 LayoutModifier 和 与他相关的 非 LayoutModifier
                val c = LayoutModifierNodeCoordinator(layoutNode, node)
                node.updateCoordinator(c)
                c
            }
            coordinator.wrappedBy = next
            next.wrapped = coordinator
            coordinator = next
        } else {
            // 挂载同一个 NodeCoordinator 下边
            node.updateCoordinator(coordinator)
        }
        node = node.parent
    }
    coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
    outerCoordinator = coordinator
}

大逻辑没有变,只是细节上的调整。syncCoordinators() 就是把

kotlin 复制代码
InnerPlaceable  ← Modifier.Node1 ← Modifier.Node2 ← ... ← Modifier.NodeN

这样一条 Modifier.Node 链,映射到

kotlin 复制代码
InnerCoordinator ← LMCoordinator1 ← LMCoordinator2 ← ... ← LMCoordinatorM ← OuterCoordinator

这样一条 NodeCoordinator 链上,其中只有那些实现了 LayoutModifier 的节点会真正生成一个新的 LayoutModifierNodeCoordinator,其它节点则共享它们最近的上层 Coordinator。这样就建立起了「谁在哪层测量/放置/绘制」的清晰对应关系。

onRemeasured 触发的原始逻辑有两处,逻辑几乎相同,他们都是自测量完成之后,再调用内部的onMeasured()。 Compose的自己测量调用方式是

kotlin 复制代码
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
    // before rerunning the user's measure block reset previous measuredByParent for children
    layoutNode.forEachChild {
        it.measuredByParent = LayoutNode.UsageByParent.NotUsed
    }

    measureResult = with(layoutNode.measurePolicy) {
        measure(layoutNode.childMeasurables, constraints)
    }
    onMeasured()
    return this
}

例如下边的代码:

kotlin 复制代码
@Composable
@Preview
fun MyComposable() {
    Text(text = "Hello World!", Modifier.onSizeChanged { })
    Text("床前明月光", Modifier.then(object : OnRemeasuredModifier {
        override fun onRemeasured(size: IntSize) {
            Log.d("zxc1 ", "onRemeasured: size = $size")
        }
    }))
    Text(
        text = "Hello World!",
      Modifier
        .padding(20.dp)
        // 它只和右边有关,因此在 padding(40.dp) 调用后就会触发这里。且它的 size 是只包含 padding(40.dp) 的尺寸。
        .then(object : OnRemeasuredModifier {
          override fun onRemeasured(size: IntSize) {
            Log.d("zxc2 ", "onRemeasured: size = $size")
          }
        })
        .padding(40.dp)
    )
}
// 输出:
onRemeasured: size = 225 x 61
onRemeasured: size = 513 x 321
kotlin 复制代码
internal val tail: Modifier.Node = innerCoordinator.tail
internal var head: Modifier.Node = tail
  • Compose 在内部会将每个 LayoutModifier 包装成一个 ModifiedLayoutNode,每遇到一个 LayoutModifier 就会划分一个新的"布局节点层";

  • 同时,所有非 LayoutModifier(包括 OnRemeasuredModifier)会分成"Before" 或 "After" 两类链表,分别记录在 LayoutNodeWrapper.entities 里。

  • 当某层的 LayoutNodeWrapper.measure(...) 完成(无论是最内层的 InnerPlaceable 还是某个 ModifiedLayoutNode)时,都会调用它的 onMeasured()

    kotlin 复制代码
    fun onMeasured() {
      if (entities.has(RemeasureEntityType)) {
        Snapshot.withoutReadObservation {
          entities.forEach(RemeasureEntityType) {
            it.modifier.onRemeasured(measuredSize)
          }
        }
      }
    }

这样就保证所有挂在"After LayoutModifier"阶段的 OnRemeasuredModifier 都能准确且及时地拿到 本层 测量结果

OnPlacedModifier 的作用、写法和原理

OnPlacedModifier 是一个 布局"放置"阶段 的回调入口。当你给某个 Composable 加上它,就能在这个节点完成放置(place(...))后、绘制前,拿到它的 LayoutCoordinates ------ 包括它在父布局坐标系/根布局坐标系中的位置、尺寸、父子层级关系等。典型用途有:

  • 读出最终位置或大小,驱动后续动画;
  • 把布局结果暴露给外部状态(比如保存某个控件的屏幕位置);
  • 结合 LocalView 做全局坐标映射,触发业务逻辑。

写法

1. 内置扩展:Modifier.onPlaced { }
kotlin 复制代码
Box(
  Modifier
    .size(100.dp)
    .onPlaced { coords ->
      // coords.positionInParent() / positionInWindow() / size / parent etc.
      Log.d("Demo", "placed at ${coords.positionInWindow()}")
    }
) {
  /*...*/
}
2. 自定义 OnPlacedModifier

如果需要更定制化,或想把逻辑封装成复用组件,可以自己实现:

kotlin 复制代码
class MyOnPlacedModifier(
  val onPlacedAction: (LayoutCoordinates) -> Unit
) : OnPlacedModifier {
  override fun onPlaced(coordinates: LayoutCoordinates) {
    onPlacedAction(coordinates)
  }
}

fun Modifier.onMyPlaced(action: (LayoutCoordinates) -> Unit): Modifier =
  this.then(MyOnPlacedModifier(action))

// 使用
Box(
  Modifier.onMyPlaced { coords ->
    // ...
  }
)

原理

  1. Modifier 链到 LayoutNodeWrapper

    Compose 会把所有实现了 LayoutModifierOnPlacedModifierOnRemeasuredModifier 等接口的 Modifier.Element,收集到内部的 LayoutNodeWrapper 上。

  2. 测量阶段

    每一层 LayoutNodeWrapper.measure(...) 负责给子节点测量尺寸。

  3. 放置阶段

    在最内层完成 place() 并返回后,框架沿着 LayoutNodeWrapper从内到外 调用:

    kotlin 复制代码
    // 伪代码示意
    wrapper.placeChildren { childPlaceable -> 
      childPlaceable.place(...)
    }
    // 放置完本层所有子项后
    if (wrapper.entities.has(PositioningEntityType)) {
      wrapper.entities.forEach(PositioningEntityType) { entity ->
        (entity.modifier as OnPlacedModifier)
          .onPlaced(wrapper.coordinates)
      }
    }

    也就是说:所有挂在这一层的 OnPlacedModifier (以及它们的扩展接口如 onPlacement, onPositioned)都会在本层放置结束后被回调一次。


小结
  • 何时用 :当你需要知道一个 Composable 最终"被放到哪里"或"占了多大区间"时,用 onPlaced 拦截。
  • 用法简单Modifier.onPlaced { coords -> ... } 或自定义 OnPlacedModifier
  • 底层原理 :属于 PositioningModifier 一类,Compose 在放置流程后自动遍历并 invoke。
相关推荐
万叶学编程10 分钟前
鸿蒙移动应用开发--渲染控制实验
前端·华为·harmonyos
艾恩小灰灰30 分钟前
深入理解CSS中的`transform-origin`属性
前端·javascript·css·html·web开发·origin·transform
ohMyGod_1231 小时前
Vue如何获取Dom
前端·javascript·vue.js
蓉妹妹1 小时前
React项目添加react-quill富文本编辑器,遇到的问题,比如hr标签丢失
前端·react.js·前端框架
码客前端1 小时前
css图片设为灰色
前端·javascript·css
艾恩小灰灰1 小时前
CSS中的`transform-style`属性:3D变换的秘密武器
前端·css·3d·css3·html5·web开发·transform-style
Captaincc1 小时前
AI coding的隐藏王者,悄悄融了2亿美金
前端·后端·ai编程
天天扭码1 小时前
一分钟解决一道算法题——矩阵置零
前端·算法·面试
抹茶san2 小时前
el-tabs频繁切换tab引发的数据渲染混淆
前端·vue.js·element
Captaincc2 小时前
关于MCP最值得看的一篇:MCP创造者聊MCP的起源、架构优势和未来
前端·mcp