创建自定义修饰符

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

修饰符包含多个部分

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

根据所需的功能,有多种方法可以实现自定义修饰符。通常,实现自定义修饰符最简单的方法是只实现一个自定义修饰符工厂,该工厂将其他已定义的修饰符工厂组合在一起。如果您需要更多自定义行为,请使用 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

一个 Modifier.Node,它接收 PointerInputChanges。

示例

ParentDataModifierNode

一个 Modifier.Node,它为父布局提供数据。

示例

LayoutAwareModifierNode

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

示例

GlobalPositionAwareModifierNode

一个 Modifier.Node,它接收一个 onGloballyPositioned 回调,其中包含布局的最终 LayoutCoordinates,此时内容的全局位置可能已更改。

示例

ObserverModifierNode

Modifier.Nodes 实现了 ObserverNode 可以提供自己的 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)
        }
    }
}

引用组合本地

Modifier.Node 修饰符不会自动观察 Compose 状态对象的更改,例如 CompositionLocalModifier.Node 修饰符相对于仅使用可组合工厂创建的修饰符的优势在于,它们可以使用 currentValueOf 从 UI 树中使用修饰符的位置读取组合本地的值,而不是从分配修饰符的位置读取。

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

此示例观察 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)更改时才使绘制失效,而不是使布局失效。这可以提高修饰符的性能。

以下是一个假设的示例,其中修饰符具有 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)
        }
    }
}