Compose编程思想 -- Compose中的经典Modifier(DrawModifier)

2 阅读7分钟

前言

在上一篇文章中,介绍了LayoutModifier的底层实现原理,以及Modifier.layout函数如何影响系统的布局流程,本节将会介绍DrawModifier的原理,从字面意思上来看是实现绘制能力的Modifier。

/**
 * A [Modifier.Element] that draws into the space of the layout.
 */
@JvmDefaultWithCompatibility
interface DrawModifier : Modifier.Element {

    fun ContentDrawScope.draw()
}

在官方的解释中:DrawModifier是用来在布局空间中执行绘制操作。

1 DrawModifier的使用

在Compose中,官方提供了drawWithContent函数用来创建一个DrawModifier,通过源码我们看到,实现方式与layout类似,都是创建了一个ModifierNodeElement实例融合,在create函数中创建真正的Node对象DrawWithContentModifier.

/**
 * Creates a [DrawModifier] that allows the developer to draw before or after the layout's
 * contents. It also allows the modifier to adjust the layout's canvas.
 */
fun Modifier.drawWithContent(
    onDraw: ContentDrawScope.() -> Unit
): Modifier = this then DrawWithContentElement(onDraw)

@OptIn(ExperimentalComposeUiApi::class)
private data class DrawWithContentElement(
    val onDraw: ContentDrawScope.() -> Unit
) : ModifierNodeElement<DrawWithContentModifier>() {
    override fun create() = DrawWithContentModifier(onDraw)

    override fun update(node: DrawWithContentModifier) = node.apply {
        onDraw = this@DrawWithContentElement.onDraw
    }

    override fun InspectorInfo.inspectableProperties() {
        name = "drawWithContent"
        properties["onDraw"] = onDraw
    }
}

其实这个部分是Compose对于Modifier做了一次收敛,当Modifier初始化的时候,会将Modifier.Element转换为Modifier.Node,这个过程中会判断element的类型,如果是ModifierNodeElement类型,例如LayoutModifierDrawModifier这种,就会执行其create函数;否则就是普通类型,统一转换为BackwardsCompatNode

1.1 drawContent

例如,我写一个Text组件,Modifier就使用drawWithContent创建一个DrawModifier,因为在Text底层是通过drawText来完成文字的绘制,因此如果不想影响系统的绘制,那么必须要调用drawContent函数,否则将不会显示任意内容。

@Composable
fun ModifierDraw() {
    Text(text = "自定义绘制",
        Modifier.drawWithContent {
            // drawContent()
        })
}

而且如果不调用drawContent,那么drawWithContent右侧的操作会被抹掉,那么为什么会造成这个现象,需要从源码角度深究。

@Composable
fun ModifierDraw() {
    Text(
        text = "自定义绘制",

        Modifier
            .size(150.dp)
            .drawWithContent {
                // drawContent()
            }
            .background(Color.Green)

    )
}

drawContent本意就是绘制内容的意思,如果没有调用,那么布局本身的内容就不会被绘制,但是自定义绘制内容将会被正常绘制。

@Composable
    fun ModifierDraw() {
        Text(
            text = "自定义绘制",

            Modifier
                .size(150.dp)
                .drawWithContent {
                    drawCircle(Color.Blue)
//                    drawContent()
                }
                .background(Color.Green)

        )
    }

如上案例,只会绘制一个圆,原先Text中的内容不会被绘制。

Screenshot_20240325_171449.png

drawContent只会影响组件自身的内容,而不会影响自定义绘制的内容,如果想要在原先组件的基础上绘制新的内容,那么可以在drawContent函数的上方或者下发自定义绘制内容。

graph LR
自定义绘制1 --> drawContent --> 自定义绘制2

绘制的层级依次升高,自定义绘制2的内容会覆盖自定义绘制1和组件自身的内容。

1.2 LayoutNode的绘制原理

ok,再回到原理这一侧,我们知道组件的测量,或者说Compose中的测量流程,是在LayoutNode的remeasure中完成,那么节点的绘制,也是在LayoutNode中完成,也就是draw函数。

internal fun draw(canvas: Canvas) = outerCoordinator.draw(canvas)

我们看到最终调用还是outerCoordinator执行draw函数,也就是说会根据Modifier.Node自身的属性绘制,例如bakground就是继承自DrawModifier

// NodeCoordinator.kt 

/**
 * Draws the content of the LayoutNode
 */
fun draw(canvas: Canvas) {
    // layer做独立绘制,一般layer为null 
    val layer = layer
    if (layer != null) {
        layer.drawLayer(canvas)
    } else {
        val x = position.x.toFloat()
        val y = position.y.toFloat()
        canvas.translate(x, y)
        drawContainedDrawModifiers(canvas)
        canvas.translate(-x, -y)
    }
}

当Modifier在初始化时,当碰到DrawModifier时,因为其继承自ModifierNodeElement,因此执行其create函数得到DrawWithContentModifier对象,加到双向链表中,因为layer一般情况下为空,那么我们着重看下没有layer的情况下,Compose是如何完成绘制的,看下drawContainedDrawModifiers函数。

private fun drawContainedDrawModifiers(canvas: Canvas) {
    val head = head(Nodes.Draw)
    if (head == null) {
        performDraw(canvas)
    } else {
        // 拿到的是
        val drawScope = layoutNode.mDrawScope
        drawScope.draw(canvas, size.toSize(), this, head)
    }
}

首先调用了head函数,其实这个函数很简单,就是遍历Coordinator,从head开始,一直遍历到tail,看是否存在至少一个Node类型为Node.Draw的节点,

inline fun <reified T> head(type: NodeKind<T>): T? {
    visitNodes(type.mask, type.includeSelfInTraversal) { return it as? T }
    return null
}

那么什么类型的Modifier是Node.Draw类型的,其实在我们之前讲Modifier初始化代码的时候,会计算其类型, 如下代码,如果Modifier.NodeDrawModifierNode类型,那么mask就是Nodes.Draw

@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(node: Modifier.Node): Int {
    var mask = Nodes.Any.mask
    if (node is LayoutModifierNode) {
        mask = mask or Nodes.Layout
    }
    if (node is DrawModifierNode) {
        mask = mask or Nodes.Draw
    }
    // ......
}

也就是说,当Modifier集合中至少存在一个DrawModifier的时候,会执行drawContainedDrawModifiers中else代码块;否则就执行performDraw函数。

1.2.1 performDraw

先来看下performDraw函数,如果在LayoutNodeNodeChain中,没有Node.Draw类型的节点,那么就会执行performDraw函数。

open fun performDraw(canvas: Canvas) {
    wrapped?.draw(canvas)
}

performDraw函数中,会执行wrapped的draw函数,wrapped是干什么的?其实在Modifier初始化的时候,详情可以看下上一篇文章第2.2小节介绍的同步操作,wrapped其实就是当前NodeCoordinator内部包裹的innerCoordinator

所以这是一个递归的过程,对于所有非Draw类型的Coordinator执行draw函数时,都会调用其内部包裹的NodeCoordinator的draw函数。

对于非Draw类型的Modifier,系统会选择性绘制其边框,其他都默认不做处理。

1.2.2 LayoutNodeDrawScope.draw

执行这个函数的时候,就说明在Modifier.Node链表中,存在至少一个DrawModifier,无论是background还是自定义的Modifier.drawWithContent

// LayoutNodeDrawScope.kt

internal fun draw(
    canvas: Canvas,
    size: Size,
    coordinator: NodeCoordinator,
    drawNode: DrawModifierNode,
) {
    val previousDrawNode = this.drawNode
    // 第一次拿到Node.Draw类型节点时,会给drawNode赋值。
    this.drawNode = drawNode
    canvasDrawScope.draw(
        coordinator,
        coordinator.layoutDirection,
        canvas,
        size
    ) {
        with(drawNode) {
            // 这里就会执行,自定义的绘制操作
            this@LayoutNodeDrawScope.draw()
        }
    }
    this.drawNode = previousDrawNode
}

注意LayoutNodeDrawScope的draw函数仅仅执行的是我们自定义的绘制操作,而如果想要绘制原内容,一定要执行drawContent函数,那么在LayoutNodeDrawScope中也有对应的函数。

1.2.3 LayoutNodeDrawScope.drawContent

所以在之前的demo用例中,如果我们不执行drawContent,那么只会绘制自定义的内容,只有执行了drawContent才会绘制本身的内容, 那么这就是原因所在。

override fun drawContent() {
    drawIntoCanvas { canvas ->
        val drawNode = drawNode!!
        // 下一个Draw类型的Node
        val nextDrawNode = drawNode.nextDrawNode()
        // NOTE(lmr): we only run performDraw directly on the node if the node's coordinator
        // is our own. This seems to work, but we should think about a cleaner way to dispatch
        // the draw pass as with the new modifier.node / coordinator structure this feels
        // somewhat error prone.
        if (nextDrawNode != null) {
            nextDrawNode.performDraw(canvas)
        } else {
            // TODO(lmr): this is needed in the case that the drawnode is also a measure node,
            //  but we should think about the right ways to handle this as this is very error
            //  prone i think
            val coordinator = drawNode.requireCoordinator(Nodes.Draw)
            val nextCoordinator = if (coordinator.tail === drawNode)
                coordinator.wrapped!!
            else
                coordinator
                
            nextCoordinator.performDraw(canvas)
        }
    }
}

因为在一个Modifier中可能会存在多个DrawModifier,所以在调用drawContent时,首先会拿到下一个Node.Draw类型的Node,如果拿到了,那么就会正常执行其内部的绘制操作,performDrawDrawModifierNode的一个扩展函数,它会执行组件的内部绘制操作。

// This is not thread safe
fun DrawModifierNode.performDraw(canvas: Canvas) {
    val coordinator = requireCoordinator(Nodes.Draw)
    val size = coordinator.size.toSize()
    val drawScope = coordinator.layoutNode.mDrawScope
    drawScope.draw(canvas, size, coordinator, this)
}

如果没有Node.Draw类型的节点,那么就不会绘制,会执行1.2.1中的performDraw函数。

@Composable
fun ModifierDraw() {
    Text(
        text = "自定义绘制",
        Modifier
            .size(150.dp)
            .background(Color.Red)
            .drawWithContent {
                drawCircle(Color.Blue)
                // drawContent()
            }
            .background(Color.Green)

    )
}

上面这个例子中,在绘制的时候,首先会拿到第一个Node.Draw类型的节点就是background,此时会绘制background的内容,因为background中执行了drawContent,此时:

drawNode : DrawModifier1(background)

nextDrawNode : DrawModifier2(drawWithContent)

graph LR
LayoutModifier --> DrawModifier1 --> DrawModifier2 --> DrawModifier3

所以会画一个圆,但是因为DrawModifier2没有执行drawContent,那么后续的DrawModifier3就不会再绘制,也就是所谓的被擦除。

background.draw{
    drawContent(){
        drawWithContent.draw{
            drawContent(){
                background.draw{
                    drawContent()
                }
            }
        }
    }
}

其实就是这样一个包裹的关系。

1.3 DrawModifier顺序敏感

通过上述源码的分析,我们看下面的例子:

@Composable
fun ModifierDraw() {
    Box(Modifier.background(Color.Blue).background(Color.Green).size(30.dp))
}

在初始化的时候:

graph LR
head --> backgroundBlue --> backgroundGreen --> size --> tail

表头为backgroundBlue,在绘制的时候会先绘制,然后才会绘制backgroundGreen,因为在visitNode的时候,会从head开始查。

所以这个Box组件最终显示的颜色就是绿色。

@Composable
fun ModifierDraw() {
    Box(
        Modifier
            .background(Color.Blue)
            .background(Color.Green)
            .requiredSize(60.dp)
            .background(Color.Gray)
            .requiredSize(20.dp)
    )
}

再看上面的例子,requiredSize属于是LayoutModifier,那么在同步NodeCoordinator的时候,background(Color.Gray)是与requiredSize(20.dp)在同一个LayoutModifierNodeCoordinator下,整体的包裹效果如下所示:

image.png

那么在测量的时候,从外层开始测量,最大值为60dp,那么会画一个60dp的绿色背景;然后带着60dp的约束条件给到下一个节点LayoutModifierNodeCoordinator进行测量,因此会判断如果大于60dp,那么就会最大限制为60dp(伙伴们可以试下),如果小于60dp,那么就是按照实际的值测量,然后绘制。

Screenshot_20240326_162152.png

通过上面的例子,我们得到了一个结论,就是右侧的LayoutModifier决定绘制区域的大小,那么再看下的例子:

@Composable
fun ModifierDraw() {
    Text(text = "测试",Modifier.size(200.dp).background(Color.Green))
}

最终显示的是一个200dp的Text,嗯?左边的LayoutModifier也会决定绘制的区域大小,其实这个只是一个错觉,因为在同步的时候,background是包裹在InnerCoordinator中的,也就是组件自身。而size作为LayoutModifier决定了组件的大小为200dp,因为在绘制的时候,也就绘制了200dp的区域。