14、自定义修饰符

2 阅读5分钟

创建自定义修饰符

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 实现自定义修饰符分为三个部分:

  1. Modifier.Node 实现:存储修饰符的逻辑和状态
  2. ModifierNodeElement:创建和更新修饰符节点实例
  3. 修饰符工厂(可选):提供公共 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 和 hashCodeModifierNodeElement 必须正确实现这两个方法,否则节点会不必要地更新,导致性能问题。
  • 不要在 update 方法中创建新节点:应更新现有节点,而不是创建新节点,这是性能提升的关键。
  • 自动失效:当对相应元素调用 update 时,节点会自动失效。可以停用此行为以更精细地控制失效。
  • 组合局部变量Modifier.Node 修饰符可以从使用位置(而非分配位置)正确读取组合本地变量。