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

前言

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

kotlin 复制代码
/**
 * 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.

kotlin 复制代码
/**
 * 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函数,否则将不会显示任意内容。

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

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

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

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

    )
}

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

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

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

        )
    }

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

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

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

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

1.2 LayoutNode的绘制原理

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

kotlin 复制代码
internal fun draw(canvas: Canvas) = outerCoordinator.draw(canvas)

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

kotlin 复制代码
// 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函数。

kotlin 复制代码
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的节点,

kotlin 复制代码
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

kotlin 复制代码
@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函数。

kotlin 复制代码
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

kotlin 复制代码
// 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才会绘制本身的内容, 那么这就是原因所在。

kotlin 复制代码
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的一个扩展函数,它会执行组件的内部绘制操作。

kotlin 复制代码
// 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函数。

kotlin 复制代码
@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就不会再绘制,也就是所谓的被擦除。

kotlin 复制代码
background.draw{
    drawContent(){
        drawWithContent.draw{
            drawContent(){
                background.draw{
                    drawContent()
                }
            }
        }
    }
}

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

1.3 DrawModifier顺序敏感

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

kotlin 复制代码
@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组件最终显示的颜色就是绿色。

kotlin 复制代码
@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下,整体的包裹效果如下所示:

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

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

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

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

相关推荐
恋猫de小郭9 分钟前
iOS 26 开始强制 UIScene ,你的 Flutter 插件准备好迁移支持了吗?
android·前端·flutter
杨筱毅11 分钟前
【底层机制】【Android】【面试】Zygote 为什么使用 Socket 而不是 Binder?
android·1024程序员节·底层机制
快乐10123 分钟前
Media3 ExoPlayer扩展FFmpeg音视频解码
android
zgyhc20501 小时前
【Android Audio】安卓音频中Surround mode切换流程
android·音视频
gfdgd xi3 小时前
Wine运行器3.4.0——虚拟机安装工具支持设置UEFI启动
android·windows·python·ubuntu·架构
shaominjin1233 小时前
OpenCV 4.1.2 SDK 静态库作用与功能详解
android·c++·人工智能·opencv·计算机视觉·中间件
东坡肘子4 小时前
Swift 官方发布 Android SDK | 肘子的 Swift 周报 #0108
android·swiftui·swift
Storm-Shadow11 小时前
Android OpenGLES视频剪辑示例源码
android·opengles·视频滤镜
双桥wow11 小时前
android 堆栈打印
android
爱学习的大牛12316 小时前
使用C++开发Android .so库的优势与实践指南
android·.so·1024程序员节