创建自定义修饰符

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.Node实现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实现还需要实现equalshashCodeupdate仅在与先前元素的相等比较返回false时才会被调用。

上面的示例使用数据类来实现这一点。这些方法用于检查节点是否需要更新。如果您的元素具有不影响节点是否需要更新的属性,或者出于二进制兼容性的原因您希望避免使用数据类,那么您可以手动实现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修饰符相对于仅使用可组合工厂创建的修饰符的优势在于,它们可以从 UI 树中使用修饰符的位置读取组合局部变量的值,而不是从分配修饰符的位置读取,使用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 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)
        }
    }
}