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 状态提供支持的 Animation 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 值在修饰符工厂的调用点进行解析
使用可组合修饰符工厂创建自定义修饰符时,CompositionLocal 会从创建它们(而非使用它们)的组合树中获取值。这可能会导致意外结果。例如,请考虑前面提到的 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 是一个较低级别的 API,用于在 Compose 中创建修饰符。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,这让它能够替换 draw 方法。
可用的类型如下:
节点 |
用法 |
示例链接 |
一种 |
||
一种可绘制到布局空间中的 |
||
实现此接口可让您的 |
||
一个 |
||
接收 PointerInputChanges 的 |
||
一个 |
||
一个接收 |
||
|
||
实现 |
||
一个 这对于将多个节点实现组合成一个节点实现非常有用。 |
||
允许 |
当在相应元素上调用更新时,节点会自动失效。由于我们的示例是 DrawModifierNode,因此每当对该元素调用 update 时,节点都会触发重新绘制,并且其颜色会正确更新。您可以选择停用自动失效功能,如停用节点自动失效功能部分中所述。
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) } } }
引用 CompositionLocal
Modifier.Node 修饰符不会自动观察 Compose 状态对象(例如 CompositionLocal)的变化。与仅使用可组合项工厂创建的修饰符相比,Modifier.Node 修饰符的优势在于,它们可以使用 currentValueOf 从界面树中使用修饰符的位置(而非分配修饰符的位置)读取 composition local 的值。
不过,修饰符节点实例不会自动观察状态变化。如需自动对 CompositionLocal 更改做出响应,您可以在某个范围内读取其当前值:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope和IntrinsicMeasureScopeSemanticsModifierNode: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 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 修饰符可以委托给其他节点。这有很多应用场景,例如跨不同修饰符提取通用实现,但也可用于跨修饰符共享通用状态。
例如,以下是共享互动数据的 clickable 修饰符节点的基本实现:
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) } } }