创建自定义修饰符

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

接收 PointerInputChange 的 Modifier.Node。

采样

ParentDataModifierNode

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

采样

LayoutAwareModifierNode

一个 Modifier.Node,用于接收 onMeasured 和 onPlaced 回调。

采样

GlobalPositionAwareModifierNode

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

采样

ObserverModifierNode

实现 ObserverNode 的 Modifier.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 实现还需要实现 equals 和 hashCode。仅当与前一个元素的相等比较返回 false 时,才会调用 update。

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

修饰符工厂

这是修饰符的公共 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)
    )
}

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

当 Modifier.Node 节点对应的 ModifierNodeElement 调用更新时,这些节点会自动失效。对于复杂的修饰符,您可能需要选择停用此行为,以便更精细地控制修饰符使阶段失效的时间。

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

以下示例展示了一个假设的示例,其中包含一个以 color、size 和 onClick 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)
        }
    }
}