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
,它允许它覆盖绘制方法。
可用的类型如下
节点 |
用法 |
示例链接 |
一个 |
||
一个 |
||
实现此接口允许您的 |
||
一个 |
||
一个 |
||
一个 |
||
一个 |
||
一个 |
||
|
||
一个 这对于将多个节点实现组合成一个节点很有用。 |
||
允许 |
当对其相应元素调用更新时,节点会自动失效。因为我们的示例是 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
实现需要覆盖以下方法
create
:这是实例化修饰符节点的函数。当首次应用修饰符时,此函数会被调用以创建节点。通常,这相当于构造节点并使用传递给修饰符工厂的参数对其进行配置。update
:每当在节点已存在的位置提供此修饰符时,但属性已更改时,此函数就会被调用。这是由类的equals
方法确定的。之前创建的修饰符节点作为参数发送给update
调用。此时,您应该更新节点的属性以对应于更新的参数。节点能够以这种方式重复使用是Modifier.Node
带来的性能提升的关键;因此,您必须更新现有节点,而不是在update
方法中创建新的节点。在我们的圆圈示例中,节点的颜色会更新。
此外,ModifierNodeElement
实现还需要实现 equals
和 hashCode
。只有当与先前元素的相等比较返回 false 时,才会调用 update
。
上面的示例使用数据类来实现这一点。这些方法用于检查节点是否需要更新。如果您的元素具有不影响节点是否需要更新的属性,或者您想出于二进制兼容性原因避免使用数据类,那么您可以手动实现 equals
和 hashCode
,例如 填充修饰符元素。
修饰符工厂
这是您的修饰符的公共 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 状态对象的更改,例如 CompositionLocal
。 Modifier.Node
修饰符相对于仅使用可组合工厂创建的修饰符的优势在于,它们可以使用 currentValueOf
从 UI 树中使用修饰符的位置读取组合本地的值,而不是从分配修饰符的位置读取。
但是,修饰符节点实例不会自动观察状态更改。要自动对组合本地更改做出反应,您可以在作用域内读取其当前值。
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
和IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
此示例观察 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
、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) } } }