Jetpack Compose 之 Modifier(上)

489 阅读32分钟

关于Modifier的其余篇章: Jetpack Compose 之 Modifier(中) Jetpack Compose 之 Modifier(下)

modifier:Modifier = Modifier 的含义

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这个接口中申明的:

image.png

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

modifier: Modifier = Modifier

这样写也是等价的:

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 参数:

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

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

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

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

@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实现的。

infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)
语法:
fun Modifier.then(other: Modifier): Modifier
用法:

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

示例:
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 的合并,使得组合后的修饰符能以最低的性能代价被执行。

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"
    } + "]"
}

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

modifier1.then(modifier2)

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

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

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

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

CombinedModifier(Modifier.background(Color.Red), Modifier.padding(8.dp))
Modifier.background(Color.Blue).then(Modifier.padding(8.dp))

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

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所利用。

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 则用于确认链中所有修饰符都满足条件。

语法:
interface Modifier.Element : Modifier

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

示例:
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 { ... } 来包装这些逻辑。
  • 示例代码

    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(或类似实现)的实例中。
private open class ComposedModifier(
    inspectorInfo: InspectorInfo.() -> Unit,
    val factory: @Composable Modifier.() -> Modifier
) : Modifier.Element, InspectorValueInfo(inspectorInfo)

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

// (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 的过程中被创建了,这个有什么区别呢?用处在哪里呢?

@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 框架内部用来管理和调度组合型修饰符的一种实现细节。


在项目中的使用

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

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 这个函数的本质。

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

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

fun Modifier.coroutineModifier() = composed {
  LaunchedEffect(key1 =, block =)
    ...Modifier
}

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

// 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 的效果。:

    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() 来构造一个自定义的布局修饰符,该修饰符在原有布局基础上增加额外的偏移量。

@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表达式才能把测量与布局切开。所以才会有下边这样的写法:

layout(placeable.width, placeable.height) {
    placeable.placeRelative(offsetX, offsetY)
}

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

layout(placeable.width, placeable.height) {
}
placeable.placeRelative(offsetX, offsetY)

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

inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {

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

image.png

只不过 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 是如何进行测量和布局的?
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 属性。

override var modifier: Modifier = Modifier
    set(value) {
        // 省略其余代码。。。。。。
        // 初始化内层的 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

        // 省略其余代码......

        // 遍历所有 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)
        }
        // 省略其余代码......
    }

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

    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

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

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

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

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

LayoutNodeWrapper 在compose中是什么?做什么用的?举个例子。 LayoutNodeWrapper 是把我们在 Modifier 链上声明的各种布局修饰符(paddingsize、自定义 layout {}、以及像 onRemeasuredonPlacedsemantics 这类插钩)“包”到实际的测量/放置流程里的那个运行时对象。它的职责就是:

  1. 串成一条链
    每遇到一个实现了 LayoutModifierModifier.Element,Compose 就会创建一个对应的 LayoutNodeWrapper,按先后顺序把它们“套”在内部的 InnerPlaceable(真正测量/放置的地方)外面。

  2. 拦截测量/放置

    • measure(constraints) 时,一个 wrapper 可以先修改传入的 constraintsmeasure 它的子项,然后再根据子项返回的 Placeable 调整最终的 layout(width, height)
    • place(x, y) 时,同理可以拦截位置,做偏移、触发 OnPlacedModifier 回调。
  3. 统一管理各种后置/前置逻辑
    比如 OnRemeasuredModifierOnPlacedModifierSemanticsModifierDrawModifier,它们都以“实体”(entity)的形式附着在 wrapper 上,框架会在合适的时机统一遍历并调用。


举个简单例子

@Composable
fun Demo() {
  Box(
    Modifier
      .padding(8.dp)                              // → 生成一个 PaddingLayoutModifier
      .size(50.dp)                                // → 生成一个 SizeLayoutModifier
      .onGloballyPositioned { coords ->           // → 生成一个 OnPlacedModifier 包装器
        Log.d("Demo", "全局位置:${coords.positionInWindow()}")
      }
  ) {
    Text("Hello")
  }
}

在内部,Compose 会把上面的 Modifier 拆成这样一条 LayoutNodeWrapper 链:

┌────────────────────────────────────────────┐
│ ModifiedLayoutNode (PaddingLayoutModifier) │      ← “padding(8dp)” 的 wrapper(最外层)
│   ├ entities = []                          │
│   └ wrappedBy →                            │
│     ┌────────────────────────────────────┐ │
│     │ ModifiedLayoutNode (SizeLayoutModifier)     ← “size(50dp)” 的 wrapper
│     │   ├ entities = []                  │ │
│     │   └ wrappedBy →                    │ │
│     │     ┌────────────────────────────┐ │ │
│     │     │ ModifiedLayoutNode (OnPlacedModifier) ← “onGloballyPositioned” 的 wrapper
│     │     │   ├ entities = [OnPlaced]  │ │ │
│     │     │   └ wrappedBy →            │ │ │
│     │     │     ┌───────────────────┐  │ │ │
│     │     │     │   InnerPlaceable  │  │ │ │      ← Text 等真实内容
│     │     │     └───────────────────┘  │ │ │
│     │     └────────────────────────────┘ │ │
│     └────────────────────────────────────┘ │
└────────────────────────────────────────────┘

  • 从整体结构上看size(50.dp) 对应的那个 ModifiedLayoutNode(我们叫它 sizeWrapper)确实是嵌套关系里的最外层一环——它“包”住了后面所有的包装器(包括 padding(8.dp) 的 wrapper、onGloballyPositioned 的 wrapper 以及最终的 InnerPlaceable)。换句话说,整个链式调用:

    Modifier
      .size(50.dp)              // ← sizeWrapper
      .padding(8.dp)            // ← paddingWrapper
      .onGloballyPositioned { } // ← onPlacedWrapper
    

    在运行时会变成一个

    sizeWrapper
      └ paddingWrapper
          └ onPlacedWrapper
              └ innerPlaceable
    

    所以,从“谁最外层”这个角度讲,sizeWrapper 确实“包含”了后面所有的 wrapper(以及实际放置内容)。

  • 但从实现细节上看,每个 wrapper 只把自己对应LayoutModifierOnPlacedModifier 实体挂到自己那层,它并不会把子层的实体都“吞”进自己的 entities 列表。也就是说:

    • sizeWrapper.entities 里只存了 SizeLayoutModifier 这一类的实体;
    • paddingWrapper.entities 里只存了 PaddingLayoutModifier
    • onPlacedWrapper.entities 里只存了 OnPlacedModifier

它们是 链式嵌套(每个 wrapper 指向下一个 wrapper),而不是“一个 wrapper 里放所有东西”。所以:

  • 宏观上size(50.dp) 的 wrapper 最外层包裹了内层所有 wrapper;
  • 微观上,它只管理自己那部分逻辑,不会把内层包装器的实体都放在自己身上。

因此:

  • LayoutNodeWrapper 就是 Compose 在底层用来把你写的每个 LayoutModifier 和相关“前置/后置”逻辑实际挂载到测量/放置流程里的桥梁。
  • 它把一条线性的 Modifier 链,拆解成一颗多层嵌套的树(或链式结构),保证每个修饰符都能在合适的时机拦截、修改、或观测布局行为。

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

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)就会怎么工作。

image.png

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

image.png

的具体实现也在LayoutModifierImpl中:

image.png

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

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,然后被居中摆放。如下图所示

image.png

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

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 修饰符:

@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 的功能。

@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 修饰符:

    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. 总结

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

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的蓝色区域:
    Box(Modifier.background(Color.Blue).padding(80.dp).size(100.dp))
    

image.png

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

Box(Modifier.background(Color.Blue).padding(80.dp).size(100.dp))

image.png

1. DrawModifier 的原理

1.1 基本概念

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

    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 也有自己的便捷函数,下边两种写法效果也是一样的

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

Modifier.then(object : DrawModifier{
  override fun ContentDrawScope.draw() {
     drawContent() // 不调用这个方法会导致之前的内容都被擦出掉
  }
})
  • 组合与链式调用:
    DrawModifier 通常以 Modifier.drawBehind、Modifier.drawWithContent 等扩展函数的形式出现,这些扩展函数内部创建了实现 DrawModifier 的对象。当我们像这样使用:

    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 的绘制流程的,是时候看看它内部的源码实现了。

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 了。):

[
   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),
]

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

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

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), 它的内部实现是这样的:

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的属性,就会变成这样:

// 默认
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)
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的话,就是层层递进执行了:

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 函数。其中测量与布局最底层用的是这个函数,不过该函数并没有提供绘制功能:

@UiComposable
@Composable inline fun Layout(
    // 定制的测量和布局的算法,所谓的原有的布局好算法, 
    // InnerPlaceable 也就是利用这个lambda 表达式完成这个操作的。
    // LayoutModifier 也可以在外部通过对该结果的进一步修改完成定制化。
    content: @Composable @UiComposable () -> Unit, 
    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)
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) {
                // 这里就是绘制我们自定义的绘制逻辑了。这样就是了为什么自定义的时候,
                // 不调用 drawContent 就会把原有内容都丢掉。因为Compose 内部真的没有对这块做任何特殊处理。
                draw() 
            }
        }
    }
}

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
}

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

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, nullnullnullnullnullnull] // 不调用drawContent,后边的都无法绘制。
    ModifiedLayoutNode(
        PaddingModifier,
        [DrawModifier3, nullnullnullnullnullnull]
        InnerPlaceable(
            [DrawModifier4->DrawModifier5->DrawModifier6, nullnullnullnullnullnull]
        )
    )
)

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

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

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

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 函数触发的。

image.png

一些示例讲解
// 红色是蓝色的背景。
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。它是做蒙版、切图、透明度渐变、阴影等效果的必备工具。

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

// 假设有一个自定义 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,并释放离屏。

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

原图:

image.png

裁剪之后的图:

image.png

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

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(),因此只绘制我们的自定义内容,内容本身之后会被默认绘制。

  • 示例:

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

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

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

  • 示例:

    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 接口。例如:

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

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

Modifier.then(CustomBorderModifier(4f, Color.Green))

3. 总结

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

学后测验

一、单选题 (3 题)

  1. Modifier.background(Color.Red).padding(8.dp) 中,红色背景的绘制范围是什么?
    A. 8 dp × 8 dp 内层区域 B. 组件最终大小减去 8 dp 内边距 C. 组件原始约束给出的完整区域 D. 不确定,取决于外层容器

    答案:C
    解析: background() 属于 DrawModifier,会挂到它右侧第一个 LayoutModifier(此处为 padding)之前,因此绘制范围等于调用 padding 之前 的测量结果,也就是组件原始可用区域。
    English: The red background draws over the whole pre-padding area.

  2. 下列哪个写法 不会 触发 LayoutNode 新建额外的 LayoutModifierNode
    A. Modifier.padding(16.dp)
    B. Modifier.size(80.dp).padding(4.dp)
    C. Modifier.background(Color.Blue).clickable {}
    D. Modifier.requiredSize(40.dp).size(80.dp)

    答案:C
    解析: 只有 LayoutModifier(如 sizepaddingrequiredSize)才会生成新的包装层。backgroundclickable 都不是 LayoutModifier,它们只挂在现有层。
    English: Only LayoutModifiers create wrappers; Draw/Pointer do not.

  3. Compose 1.5 之后,将“同类节点存储结构”从 数组 改为 单向链表 的主要收益是:
    A. 避免空槽浪费、降低内存 B. 缩短 Modifier 链代码 C. 减少线程切换 D. 提高图片解码速度

    答案:A
    解析: 单链表+全局槽位数组减少了每层小数组对象和大量 null 空位,显著降低内存占用并提高缓存命中率。
    English: Memory & cache efficiency.


二、多选题 (2 题)

  1. 关于 Modifier.composed { … } 说法正确的是(多选):
    ☐ A. 可以在其中调用 remember 保存状态
    ☐ B. 工厂 Lambda 在每次 then 组合时立即执行
    ☐ C. 返回值最终被替换为 Lambda 内部生成的新 Modifier
    ☐ D. 会比普通 Modifier 引入额外重组(Recomposition)成本

    答案:A C D
    解析:

    • A:composed 提供组合上下文,可用 remember
    • B:Lambda 延迟,在材质化 (materialize) 阶段执行。
    • C:执行结果替换原 ComposedModifier
    • D:因参与组合,状态变化会触发重组,略增开销。
      English: B is wrong because the factory is lazy.
  2. 下列哪些行为会导致 删除并重新创建 一个已存在的 LayoutModifierNode
    ☐ A. 把 Modifier.padding(8.dp) 改成 Modifier.border(1.dp, Color.Black)
    ☐ B. 动态改变 sizeState 以更新 Modifier.size(sizeState.dp)
    ☐ C. 交换同一行里 size()padding() 的先后顺序
    ☐ D. 在 Modifier.clickable {} 之后追加 .pointerInput { … }

    答案:A C
    解析:

    • A:节点类型从 PaddingBorder,需重建。
    • C:链位置变化触发删除+插入。
    • B:参数变更仅调用 update() 复用节点。
    • D:clickablepointerInput 都是 Pointer 类节点;追加只是在链尾插入新节点,不影响旧节点。
      English: Only type or position change forces re-creation.

三、判断题 (2 题)

  1. Modifier.alpha(0.5f) 一定会为组件创建 GPU Layer。
    答案:对
    解析: alpha() 内部调用 graphicsLayer(alpha = …, clip = true),而 graphicsLayer 总会生成一个 RenderNode 层以便独立处理透明度。
    English: graphicsLayer allocates a layer.
  2. 如果 DrawModifierdraw() 中不调用 drawContent(),则组件和其所有子节点都不会被绘制。
    答案:错
    解析: 只会导致当前位置之后DrawModifier 以及基础内容被跳过;外层之前的绘制仍然执行。
    English: It skips inner content, not anything drawn earlier.

四、简答题 (2 题)

  1. 为什么 Compose 同时保留 “全局槽位数组 + 单向链表” 而不用纯链表?
    答案: 槽位数组 head[kind] 让框架在布局、绘制、输入阶段可以 O(1) 定位首个目标节点;随后按 next 顺序遍历链表即可。纯链表需线性查找每种 kind,时间复杂度高。
    English: Array gives constant-time entry points; list keeps memory small.

  2. 说明 Modifier.layout { measurable, constraints -> … } 能做和不能做的事。
    答案:

    • 能做: 修改传入子组件的约束、测量子组件、改变最终 layout(width, height) 并在内部偏移 placeable,实现额外内边距、对齐、简单位移等。
    • 不能做: 像传统 ViewGroup#onLayout 那样逐个摆放多个子组件;Modifier 只面向单一可测量体。复杂多子布局需用 Layout 组合或自定义 MeasurePolicy
      English: LayoutModifier can intercept one child measure/place but can’t lay out multiple children.

五、编程题 (1 题)

  1. 实现一个可重用的 Modifier.paddingOutline(color, thickness),效果: 先绘制带圆角的描边,再在内部添加 8 dp 内边距,不影响点击区域。要求:
  • 使用 Modifier.composed { … }
  • 点击区域应为“描边内”区域
  • 圆角固定 6 dp
fun Modifier.paddingOutline(
    color: Color,
    thickness: Dp
) = composed {
    val strokePx = with(LocalDensity.current) { thickness.toPx() }
    val corner = with(LocalDensity.current) { 6.dp.toPx() }
    this
        // ① 描边属于 DrawModifier,挂到后面的 LayoutModifier 之前
        .drawWithContent {
            drawRoundRect(
                color = color,
                size = size,
                cornerRadius = CornerRadius(corner, corner),
                style = Stroke(width = strokePx)
            )
            drawContent()
        }
        // ② 内边距属于 LayoutModifier,创建新包装层,点击区域即为内层尺寸
        .padding(8.dp)
}

解析:
drawWithContent 先画圆角描边再 drawContent(),保证子内容在描边内。随后 padding(8.dp) 新建 LayoutModifierNode,把 DrawModifier 固定到外层,触控区域自动缩为描边内部。
English: Compose a DrawModifier plus a LayoutModifier; padding determines hit-test bounds.