创建自定义修饰符

Compose 开箱即用,可为常见行为提供许多修饰符,但您也可以创建自己的自定义修饰符。

修饰符包含多个部分:

  • 修饰符工厂
    • 这是 Modifier 的扩展函数,可为修饰符提供惯用的 API,并允许轻松地将修饰符链接在一起。修饰符工厂会生成 Compose 用于修改界面的修饰符元素。
  • 修饰符元素
    • 您可以在此处实现修饰符的行为。

您可以根据所需的功能,通过多种方式实现自定义修饰符。通常,实现自定义修饰符的最简单方法是仅实现一个自定义修饰符工厂,该工厂将其他已定义的修饰符工厂组合在一起。如果您需要更自定义的行为,请使用 Modifier.Node API 实现修饰符元素,这些 API 级别较低,但可提供更高的灵活性。

将现有修饰符链接在一起

通常,只需使用现有修饰符即可创建自定义修饰符。例如,Modifier.clip() 是使用 graphicsLayer 修饰符实现的。此策略使用现有的修饰符元素,并由您提供自己的自定义修饰符工厂。

在实现自己的自定义修饰符之前,请先看看是否可以使用相同的策略。

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

或者,如果您发现自己经常重复使用同一组修饰符,可以将它们封装到自己的修饰符中:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

使用可组合的修饰符工厂创建自定义修饰符

您还可以使用可组合函数创建自定义修饰符,以将值传递给现有修饰符。这称为可组合的修饰符工厂。

使用可组合的修饰符工厂创建修饰符还允许使用更高级别的 Compose API,例如 animate*AsState 和其他由 Compose 状态支持的动画 API。例如,以下代码段展示了一个修饰符,该修饰符可在启用/停用时为 Alpha 变化添加动画效果:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

如果您的自定义修饰符是一种用于从 CompositionLocal 提供默认值的便捷方法,则实现此目的的最简单方法是使用可组合的修饰符工厂:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

这种方法有一些注意事项,详见下文。

CompositionLocal 值在修饰符工厂的调用点解析

使用可组合修饰符工厂创建自定义修饰符时,组合本地变量会从创建它们的组合树中获取值,而不是从使用它们的组合树中获取值。这可能会导致意外结果。例如,采用上述局部修饰符示例,使用可组合函数以略微不同的方式实现:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

如果修饰符的运作方式与您的预期不符,请改用自定义 Modifier.Node,因为组合本地变量将在使用位置正确解析,并且可以安全地提升。

可组合函数修饰符永远不会被跳过

可组合的工厂修饰符永远不会被跳过,因为具有返回值的可组合函数无法跳过。这意味着您的修饰符函数会在每次重组时被调用,如果重组频繁,这可能会带来较高的开销。

可组合函数修饰符必须在可组合函数内调用

与所有可组合函数一样,可组合工厂修饰符必须从组合内调用。这限制了修饰符可提升到的位置,因为修饰符永远无法提升到组合之外。相比之下,非可组合修饰符工厂可以从可组合函数中提升出来,以便更轻松地重复使用并提高性能:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

使用 Modifier.Node 实现自定义修饰符行为

Modifier.Node 是用于在 Compose 中创建修饰符的较低级别 API。Compose 也是通过此 API 实现自己的修饰符的,这是创建自定义修饰符的最有效方式。

使用 Modifier.Node 实现自定义修饰符

使用 Modifier.Node 实现自定义修饰符分为三个部分:

  • 一种 Modifier.Node 实现,用于保存修饰符的逻辑和状态。
  • 用于创建和更新修饰符节点实例的 ModifierNodeElement
  • 可选的修饰符工厂,如上所述。

ModifierNodeElement 类是无状态的,每次重组都会分配新实例,而 Modifier.Node 类可以是有状态的,可以在多次重组中存活下来,甚至可以重复使用。

以下部分将介绍每个部分,并展示一个构建自定义修饰符以绘制圆形的示例。

Modifier.Node

Modifier.Node 实现(在此示例中为 CircleNode)实现了自定义修饰符的功能。

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

在此示例中,它会绘制具有传递给修饰符函数的颜色的圆。

节点实现 Modifier.Node 以及零个或多个节点类型。根据修饰符所需的功能,节点类型有所不同。上述示例需要能够进行绘制,因此它实现了 DrawModifierNode,这使其能够替换绘制方法。

可用的类型如下:

节点

用法

示例链接

LayoutModifierNode

一种 Modifier.Node,用于更改其封装内容的测量和布局方式。

示例

DrawModifierNode

一种可绘制到布局空间的 Modifier.Node

示例

CompositionLocalConsumerModifierNode

实现此接口可让您的 Modifier.Node 读取组合本地变量。

示例

SemanticsModifierNode

一个 Modifier.Node,用于添加语义键值对,以便用于测试、无障碍功能和类似用例。

示例

PointerInputModifierNode

接收 PointerInputChangeModifier.Node

示例

ParentDataModifierNode

一种向父布局提供数据的 Modifier.Node

示例

LayoutAwareModifierNode

一个 Modifier.Node,用于接收 onMeasuredonPlaced 回调。

示例

GlobalPositionAwareModifierNode

一个 Modifier.Node,当内容的全局位置可能发生变化时,它会收到一个 onGloballyPositioned 回调,其中包含布局的最终 LayoutCoordinates

示例

ObserverModifierNode

实现 ObserverNodeModifier.Node 可以提供自己的 onObservedReadsChanged 实现,该实现将在响应 observeReads 块内读取的快照对象的更改时调用。

示例

DelegatingNode

能够将工作委托给其他 Modifier.Node 实例的 Modifier.Node

这有助于将多个节点实现组合为一个。

示例

TraversableNode

允许 Modifier.Node 类在节点树中向上/向下遍历,以查找相同类型的类或特定键。

示例

当对相应元素调用更新时,节点会自动失效。由于我们的示例是 DrawModifierNode,因此每次对元素调用更新时,节点都会触发重绘,并且其颜色会正确更新。您可以选择停用自动失效功能,详情请参阅下文

ModifierNodeElement

ModifierNodeElement 是一个不可变的类,用于保存创建或更新自定义修饰符的数据:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

ModifierNodeElement 实现需要替换以下方法:

  1. create:此函数用于实例化修饰符节点。当首次应用修饰符时,系统会调用此函数来创建节点。通常,这相当于构建节点并使用传递给修饰符工厂的参数对其进行配置。
  2. update:每当此修饰符在节点已存在的同一位置提供,但属性已更改时,系统就会调用此函数。这由该类的 equals 方法确定。之前创建的修饰符节点作为参数发送到 update 调用。此时,您应更新节点的属性,使其与更新后的参数相对应。节点以这种方式重复使用的能力是 Modifier.Node 带来性能提升的关键;因此,您必须更新现有节点,而不是在 update 方法中创建新节点。在我们的圆形示例中,节点的颜色已更新。

此外,ModifierNodeElement 实现还需要实现 equalshashCode。仅当与前一个元素的相等比较返回 false 时,才会调用 update

上述示例使用数据类来实现此目的。这些方法用于检查节点是否需要更新。如果您的元素具有不影响节点是否需要更新的属性,或者您出于二进制兼容性原因而要避免使用数据类,则可以手动实现 equalshashCode,例如内边距修饰符元素

修饰符工厂

这是修饰符的公共 API 表面。大多数实现只需创建修饰符元素并将其添加到修饰符链中:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

完整示例

这三部分共同构成了使用 Modifier.Node API 绘制圆形的自定义修饰符:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

使用 Modifier.Node 的常见情况

使用 Modifier.Node 创建自定义修饰符时,您可能会遇到以下一些常见情况。

零形参

如果修饰符没有形参,则永远不需要更新,并且不需要是数据类。下面是一个修饰符的实现示例,该修饰符可将固定数量的内边距应用于可组合项:

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            placeable.place(paddingPx, paddingPx)
        }
    }
}

引用 CompositionLocal

Modifier.Node 修饰符不会自动观察对 Compose 状态对象(如 CompositionLocal)的更改。与仅使用可组合函数工厂创建的修饰符相比,Modifier.Node 修饰符的优势在于,它们可以使用 currentValueOf 从界面树中修饰符的使用位置(而非修饰符的分配位置)读取本地组合的值。

不过,修饰符节点实例不会自动观察状态变化。如需自动对本地组合的更改做出反应,您可以在作用域内读取其当前值:

此示例会观测 LocalContentColor 的值,以根据其颜色绘制背景。由于 ContentDrawScope 会观察快照更改,因此当 LocalContentColor 的值发生更改时,系统会自动重新绘制:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

如需对范围外的状态变化做出反应并自动更新修饰符,请使用 ObserverModifierNode

例如,Modifier.scrollable 使用此技术来观察 LocalDensity 中的变化。以下是一个简化示例:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // Place holder fling behavior, we'll initialize it when the density is available.
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // monitor change in Density
    }

    override fun onObservedReadsChanged() {
        // if density changes, update the default fling behavior.
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

动画修饰符

Modifier.Node 实现可以访问 coroutineScope。这样便可使用 Compose Animatable API。例如,以下代码段修改了上面的 CircleNode,使其反复淡入和淡出:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private lateinit var alpha: Animatable<Float, AnimationVector1D>

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        alpha = Animatable(1f)
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

使用委托在修饰符之间共享状态

Modifier.Node 修饰符可以委托给其他节点。这有很多应用场景,例如提取不同修饰符之间的通用实现,但它也可用于在修饰符之间共享通用状态。

例如,一个共享互动数据的可点击修饰符节点的基本实现:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

选择停用节点自动失效功能

ModifierNodeElement 调用更新时,Modifier.Node 节点会自动失效。有时,在更复杂的修饰符中,您可能希望选择停用此行为,以便更精细地控制修饰符使阶段失效的时间。

如果您的自定义修饰符同时修改布局和绘制,此功能会特别有用。选择停用自动失效功能后,您只需在仅更改与绘制相关的属性(例如 color)时使绘制失效,而无需使布局失效。这有助于提升修饰符的效果。

下面是一个假设的示例,其中包含一个以 colorsizeonClick lambda 作为属性的修饰符。此修饰符仅使必要的内容失效,并跳过任何不必要的失效:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
    override val shouldAutoInvalidate: Boolean
        get() = false

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // Only invalidate draw when color changes
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // Only invalidate layout when size changes
            invalidateMeasurement()
        }

        // If only onClick changes, we don't need to invalidate anything
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}