创建自定义修饰符
Compose 开箱即用提供了许多修饰符,但您也可以创建自己的自定义修饰符。修饰符由多个部分组成:
- 修饰符工厂:
Modifier上的扩展函数,提供惯用 API,允许修饰符轻松链接 - 修饰符元素:实现修饰符行为的部分
通常,实现自定义修饰符的最简单方法是组合其他已定义的修饰符。如果需要更多自定义行为,可以使用 Modifier.Node API。
将现有修饰符链接在一起
实现自定义修饰符最简单的方式是组合现有修饰符:
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
或者,如果经常重复使用同一组修饰符,可以将它们封装到自己的修饰符中:
fun Modifier.myBackground(color: Color) = padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(color)
使用可组合项修饰符工厂创建自定义修饰符
可以使用可组合函数创建自定义修饰符,将值传递给现有修饰符:
@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
}
}
警告:创建自定义修饰符时,不要中断修饰符链。必须始终引用 this,否则之前添加的任何修饰符都会被舍弃。
如果修饰符是从 CompositionLocal 提供默认值的便捷方法:
@Composable
fun Modifier.fadedBackground(): Modifier {
val color = LocalContentColor.current
return this then Modifier.background(color.copy(alpha = 0.5f))
}
注意:使用可组合项修饰符工厂时,组合局部变量会从其创建(而非使用)的组合树中获取值,这可能导致意外结果:
@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 实现自定义修饰符
Modifier.Node 是创建自定义修饰符的性能最高的方式(比已废弃的 composed{} API 性能好得多)。
使用 Modifier.Node 实现自定义修饰符分为三个部分:
- Modifier.Node 实现:存储修饰符的逻辑和状态
- ModifierNodeElement:创建和更新修饰符节点实例
- 修饰符工厂(可选):提供公共 API
示例:绘制圆形的自定义修饰符
Modifier.Node 实现:
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
override fun ContentDrawScope.draw() {
drawCircle(color)
}
}
ModifierNodeElement:
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
override fun create() = CircleNode(color)
override fun update(node: CircleNode) {
node.color = color
}
}
修饰符工厂:
fun Modifier.circle(color: Color) = this then CircleElement(color)
完整实现:
// 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 的常见情况
1. 零参数
如果修饰符没有参数,永远不需要更新,也不需要是数据类:
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)
}
}
}
2. 引用组合本地变量
Modifier.Node 修饰符不会自动观察 Compose 状态对象。可以从界面树中使用修饰符的位置读取组合本地值:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode {
override fun ContentDrawScope.draw() {
val currentColor = currentValueOf(LocalContentColor)
drawRect(color = currentColor)
drawContent()
}
}
对作用域之外的状态更改做出响应:
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)
}
}
3. 动画修饰符
Modifier.Node 实现可以访问 coroutineScope,可以使用 Compose Animatable API:
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)
)
}
}
}
4. 使用委托在修饰符之间共享状态
Modifier.Node 修饰符可以委托给其他节点,在修饰符之间共享常见状态:
class ClickableNode : DelegatingNode() {
val interactionData = InteractionData()
val focusableNode = delegate(FocusableNode(interactionData))
val indicationNode = delegate(IndicationNode(interactionData))
}
5. 停用节点自动失效功能
停用自动失效后,可以更精细地控制修饰符何时使阶段失效:
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)
}
}
}
修饰符节点类型参考
| 节点类型 | 用法 |
|---|---|
| LayoutModifierNode | 用于更改其封装内容的测量和布局方式 |
| DrawModifierNode | 用于绘制到布局空间 |
| CompositionLocalConsumerModifierNode | 用于读取组合本地变量 |
| SemanticsModifierNode | 用于添加语义键值对,用于测试、无障碍功能等 |
| PointerInputModifierNode | 接收 PointerInputChange 事件 |
| ParentDataModifierNode | 向父布局提供数据 |
| LayoutAwareModifierNode | 用于接收 onMeasured 和 onPlaced 回调 |
| GlobalPositionAwareModifierNode | 用于获取全局位置变化回调 |
| ObserverModifierNode | 可以提供自己的 onObservedReadsChanged 实现 |
| DelegatingNode | 能够将工作委托给其他 Modifier.Node 实例 |
| TraversableNode | 允许向上/向下遍历节点树 |
重要注意事项
- 正确实现 equals 和 hashCode:
ModifierNodeElement必须正确实现这两个方法,否则节点会不必要地更新,导致性能问题。 - 不要在 update 方法中创建新节点:应更新现有节点,而不是创建新节点,这是性能提升的关键。
- 自动失效:当对相应元素调用 update 时,节点会自动失效。可以停用此行为以更精细地控制失效。
- 组合局部变量:
Modifier.Node修饰符可以从使用位置(而非分配位置)正确读取组合本地变量。