创建自定义修饰符

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,因为 CompositionLocal 将是 可在使用现场正确解决,并且可以安全提升。

绝不会跳过可组合函数修饰符

从不跳过可组合工厂修饰符,因为可组合函数 返回的值无法跳过。这意味着您的修饰符函数 每次重组时都会调用,如果重组,费用可能会很高 。

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

与所有可组合函数一样,必须从 。这会限制修饰符可以提升到的位置,因为它可以 绝不会从组合中提升。相比之下,不可组合修饰符 工厂可以从可组合函数中提升出来,以便更轻松地重复使用, 提升效果:

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 读取 CompositionLocal。

示例

SemanticsModifierNode

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

示例

PointerInputModifierNode

一个 Modifier.Node,用于接收 PointerInputChanges.

示例

ParentDataModifierNode

为父布局提供数据的 Modifier.Node

示例

LayoutAwareModifierNode

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

示例

GlobalPositionAwareModifierNode

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

示例

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 实现还需要实现 equalshashCodeupdate 只有在与 返回 false。

上面的示例使用数据类来实现这一点。这些方法用于 检查节点是否需要更新。如果元素的属性 不会影响节点是否需要更新, 类,那么您可以手动实现 equalshashCode,例如内边距修饰符元素

修饰符工厂

这是修饰符的公共 API Surface。大部分植入方式 创建修饰符元素并将其添加到修饰符链中:

// 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 状态的变化 对象,例如 CompositionLocalModifier.Node 修饰符的优势 使用可组合工厂创建的修饰符的优点是,它们可以读取 在界面中使用修饰符的 CompositionLocal 的值 而不是在分配修饰符的位置,使用 currentValueOf

不过,修饰符节点实例不会自动观察状态变化。接收者 自动响应 CompositionLocal 的变化,您可以读取其当前 某个范围内的值:

此示例会观察 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 val alpha = Animatable(1f)

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

    override fun onAttach() {
        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 的修饰符, sizeonClick 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)
        }
    }
}