当前位置: 首页 > article >正文

Compose 实践与探索九 —— DrawModifier 解析

本篇讲解 DrawModifier 的基本用法与代码原理,介绍原理的目的在于可以判断绘制与绘制的关系,绘制与布局的关系。知道达成某种绘制效果应该怎么写,面对复杂的 Modifier 链时对效果有大致预判。

DrawModifier 管理绘制,需要以负责管理测量和布局的 LayoutModifier 为前置知识。先看一个与 LayoutModifier 相关的例子:

Box(Modifier.requiredSize(80.dp).background(Color.Blue).requiredSize(40.dp))

按照之前讲过的 LayoutModifier 的知识,尺寸应以 Modifier 链的左侧为准,也就是 80dp。但实际运行结果是 40dp,因为 background() 内的 Background 是一个 DrawModifier,DrawModifier 的结果要看 Modifier 链的右侧。

那具体的 DrawModifier 的使用与原理如何,我们一步步来看。

1、DrawModifier 的基本用法

DrawModifier 接口内只有一个函数 draw():

@JvmDefaultWithCompatibility
interface DrawModifier : Modifier.Element {
    fun ContentDrawScope.draw()
}

使用上,可以通过 then() 连接一个 DrawModifier 的匿名对象:

Modifier.then(object : DrawModifier {
    override fun ContentDrawScope.draw() {
        // 绘制内容...
    }
})

或者使用简便函数 drawWithContent():

fun Modifier.drawWithContent(
    onDraw: ContentDrawScope.() -> Unit
): Modifier = this.then(
    DrawWithContentModifier(
        onDraw = onDraw,
        inspectorInfo = debugInspectorInfo {
            name = "drawWithContent"
            properties["onDraw"] = onDraw
        }
    )
)

DrawWithContentModifier 在实现 DrawModifier 时实际上就是调用了参数上传入的 onDraw:

private class DrawWithContentModifier(
    val onDraw: ContentDrawScope.() -> Unit,
    inspectorInfo: InspectorInfo.() -> Unit
) : DrawModifier, InspectorValueInfo(inspectorInfo) {

    // 实现 DrawModifier 接口函数,调用参数传入的 onDraw() 实现绘制
    override fun ContentDrawScope.draw() {
        onDraw()
    }
}

因此两种使用方式其实是等价的。

在使用上述两种方式进行绘制时,需要注意,如果你想在原有绘制内容的基础上再多绘制一些内容,你需要调用 drawContent():

Modifier.drawWithContent { 
    // 绘制原有内容
    drawContent() 
    // 多绘制的内容,比如画一个圆,后绘制的内容会覆盖先绘制的 drawContent()
    drawCircle(Color.Red)
}

如果不调用 drawContent() 绘制原有内容,drawWithContent() 绘制的内容会覆盖原有内容导致原有内容不被绘制。原有内容是指位于 Modifier 链中 drawWithContent() 右侧的内容。比如说:

Box(Modifier.background(Color.Blue).size(40.dp).drawWithContent { })
Box(Modifier.size(40.dp).drawWithContent { }.background(Color.Blue))
Box(Modifier.size(40.dp).drawWithContent { drawContent() }.background(Color.Blue))

三种写法的结果如下:

  • 第一种写法会显示一个 40dp 的蓝色 Box,因为空的 drawWithContent() 右侧没有其他修饰符了,相当于它没有覆盖任何内容
  • 第二种写法不会显示任何内容,因为空的 drawWithContent() 覆盖了其右侧的 background(Color.Blue),使得 Box 没有背景色了,所以显示不出任何内容
  • 第三种写法会显示一个 40dp 的蓝色 Box,因为在 drawWithContent() 内调用了 drawContent(),让其右侧的 background(Color.Blue) 的内容得以绘制

drawContent() 是 ContentDrawScope 接口的函数:

@JvmDefaultWithCompatibility
interface ContentDrawScope : DrawScope {
	fun drawContent()
}

因此,该函数的调用者类型只能是 ContentDrawScope,刚好 drawWithContent() 的 onDraw 参数与 DrawWithContentModifier 的构造函数的第一个参数 onDraw 指定的接收者类型就是 ContentDrawScope,所以可以直接在 drawWithContent() 内调用 drawContent()。

在 drawWithContent() 内调用 drawContent() 才能绘制原有内容这种用法,虽然相比于让 Compose 直接帮我们绘制原有内容要多调用一个 drawContent(),稍微麻烦一点点,但是自由度却大大增加了。我们可以根据业务需求定制绘制的内容与覆盖顺序:

Box(
    Modifier
    .size(40.dp)
    .drawWithContent {
        // 业务绘制 1
        drawContent()
        // 业务绘制 2
    }
    .background(Color.Blue)
)

先绘制的内容会被后绘制的内容覆盖,可以利用这一点定制绘制内容的覆盖关系。

此外,由于 ContentDrawScope 父接口 DrawScope 内提供了绘制函数 drawLine()、drawRect()、drawImage()、drawCircle() 等,因此在 drawWithContent() 中也可以直接使用这些函数:

Box(
    Modifier
    .size(40.dp)
    .drawWithContent {
        drawContent()
        // 在蓝色 Box 上面画一个红色的圆
        drawCircle(Color.Red)
    }
    .background(Color.Blue)
)

至于业务绘制的具体代码,需要使用 Compose 的 Canvas,它内部使用了 Android 原生的 Canvas,二者在使用上会有一些区别。比如,Compose 的 Canvas 在使用上会方便一些,但是没提供绘制文字的函数 drawText(),只能通过 Compose 的 Canvas 拿到底层 Android 原生的 Canvas 再绘制文字。当然,具体内容会在后续介绍自定义绘制时再详细介绍。

接下来我们会介绍绘制原理,主要介绍两个部分,一是 DrawModifier 是如何在 Modifier 链中被识别出来并处理的,二是具体的绘制过程。

2、DrawModifier 的处理

前面在讲 LayoutModifier 原理时,讲到它是在 LayoutNode 的 modifier 属性的 set() 中进行处理,DrawModifier 也是在这个位置:

    override var modifier: Modifier = Modifier
        set(value) {
            ...
            val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
                ...

                // DrawModifier 生效位置             
                toWrap.entities.addBeforeLayoutModifier(toWrap, mod)

                ...

                val wrapper = if (mod is LayoutModifier) {
                    // Re-use the layoutNodeWrapper if possible.
                    (reuseLayoutNodeWrapper(toWrap, mod)
                        ?: ModifiedLayoutNode(toWrap, mod)).apply {
                        onInitialize()
                        updateLookaheadScope(mLookaheadScope)
                    }
                } else {
                    toWrap
                }
                wrapper.entities.addAfterLayoutModifier(wrapper, mod)
                wrapper
            }

            setModifierLocals(value)

            outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
            // foldOut() 的结果 outerWrapper 替换掉 layoutDelegate.outerWrapper
            layoutDelegate.outerWrapper = outerWrapper
            ...
        }

其实入口代码就一句 toWrap.entities.addBeforeLayoutModifier(toWrap, mod) 将 DrawModifier 放到一个元素是链表的数组中。那具体代码,我们一步一步来看。

2.1 toWrap 与 mod

这两个变量是 foldOut() 的 lambda 表达式的参数,它们的具体含义在上一篇讲解 LayoutModifier 时已经详细说明过。这里为了让读者看起来方便一些,我们再简单地回顾一下。

foldOut() 逆向(从右向左)遍历 modifier 链,初始值传入的是 innerLayoutNodeWrapper:

    internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
    internal val layoutDelegate = LayoutNodeLayoutDelegate(this, innerLayoutNodeWrapper)
    internal val outerLayoutNodeWrapper: LayoutNodeWrapper
	    // outerWrapper 是 LayoutNodeLayoutDelegate() 的第二个参数 innerLayoutNodeWrapper
        get() = layoutDelegate.outerWrapper

它的编译时类型为 LayoutNodeWrapper,运行时类型则为 InnerPlaceable。InnerPlaceable 内部不包含其他 LayoutNodeWrapper,它只负责组件自身的测量。

接下来,进入 foldOut() 的 lambda 表达式,参数 mod 就是本次遍历到的 Modifier 对象,一般是 Modifier.Element 的实现类,而 toWrap 是上次遍历的结果。影响遍历结果的因素是 LayoutModifier,在计算 wrapper 的位置,我们能看到,如果 mod 是一个 LayoutModifier,那么就用 LayoutNodeWrapper 的另一个子类 ModifiedLayoutNode 将 toWrap 与 mod 包装起来:

@OptIn(ExperimentalComposeUiApi::class)
internal class ModifiedLayoutNode(
    override var wrapped: LayoutNodeWrapper,
    var modifier: LayoutModifier
) : LayoutNodeWrapper(wrapped.layoutNode)

就是说,遍历时遇到 LayoutModifier 就会把之前的遍历结果与这个 Modifier 包装到 ModifiedLayoutNode 中,这样就会形成一个层级结构,我们通过举例来说明。比如说对于如下的组件:

Text("Compose", Modifier.padding(10.dp).padding(20.dp))

foldOut() 在进行遍历时,首先就遍历到初始值,也就是负责 Text() 自身测量逻辑的 innerLayoutNodeWrapper。然后遍历到最右侧的 padding(20.dp),PaddingModifier 是 LayoutModifier 的实现类,因此需要用 ModifiedLayoutNode 将 PaddingModifier 与 innerLayoutNodeWrapper 包装起来:

ModifiedLayoutNode(
	padding(20.dp) - PaddingModifier,
	innerLayoutNodeWrapper - InnerPlaceable
)

接下来遍历到 padding(10.dp),还是同样的套路:

ModifiedLayoutNode(
	padding(10.dp) - PaddingModifier,
	ModifiedLayoutNode(
        padding(20.dp) - PaddingModifier,
        innerLayoutNodeWrapper - InnerPlaceable
    )
)

这样 mod 与 toWrap 两个参数的含义就很清晰了:mod 是本次遍历的 Modifier 对象,toWrap 是上一次遍历的结果,它是一个 LayoutNodeWrapper,如果 Modifier 链上没有 LayoutModifier,那么它就是初始值的类型 InnerPlaceable,否则它就是 ModifiedLayoutNode。并且,如果有多个 LayoutModifier 的话,这个 ModifiedLayoutNode 内部还嵌套其他 ModifiedLayoutNode。

实际上 LayoutNodeWrapper 就只有两个子类型 InnerPlaceable 和 ModifiedLayoutNode。

在 modifier 属性的 set() 中,foldOut() 遍历的结果 outerWrapper 替换掉了 layoutDelegate.outerWrapper。我们再看 outerLayoutNodeWrapper 的定义,它的 get() 正是 layoutDelegate.outerWrapper。所以,上面这个层级结构实际上就是 outerLayoutNodeWrapper 的结构。

2.2 将 DrawModifier 添加到链表中

弄清了 mod 与 toWrap 的含义,确认了 toWrap 是一个 LayoutNodeWrapper,但是在不同情况下具体类型不同之后,我们要看 toWrap.entities.addBeforeLayoutModifier(toWrap, mod) 的后两步 —— entities 属性与 addBeforeLayoutModifier 函数。

LayoutNodeWrapper 的 entities 属性是一个 EntityList,用于保存与当前 LayoutNodeWrapper 相关联的 LayoutNodeEntity:

val entities = EntityList()

上面这样初始化,是让 EntityList 的 entities 属性取了默认值,一个容量为 7 的空数组:

@kotlin.jvm.JvmInline
internal value class EntityList(
    // TypeCount 常量为 7,说明有 7 种类型
    val entities: Array<LayoutNodeEntity<*, *>?> = arrayOfNulls(TypeCount)
)

数组的元素类型为 LayoutNodeEntity,这是一个链表结构,内部有 next 指针:

/**
* 在 LayoutNodeWrapper 中的实体的基类。一个 LayoutNodeEntity 是一个链接列表中的节点,
* 可以从 EntityList 中引用。
*/
internal open class LayoutNodeEntity<T : LayoutNodeEntity<T, M>, M : Modifier>(
    val layoutNodeWrapper: LayoutNodeWrapper,
    val modifier: M
) {
   /**
     * 列表中的下一个元素。next 是被此 LayoutNodeEntity 包裹的元素。
     */
    var next: T? = null
}

这里我们就能看出 EntityList 这个数据结构与 JDK 1.8 之前的 HashMap 结构非常像,都是数组 + 链表的结构。EntityList 是开辟了容量为 7 的数组,数组内每个元素都是一个 LayoutNodeEntity 类型的链表(头)。

LayoutNodeEntity 有四个子类:DrawEntity、PointerInputEntity、SemanticsEntity 和 SimpleEntity,分别用于保存 DrawModifier、PointerInputModifier、SemanticsModifier 和 ParentDataModifier。这四种类型,刚好是 addBeforeLayoutModifier() 内处理的类型:

@kotlin.jvm.JvmInline
internal value class EntityList(
    val entities: Array<LayoutNodeEntity<*, *>?> = arrayOfNulls(TypeCount)
) {
    /**
     * 为 [modifier] 支持的类型添加 [LayoutNodeEntity] 值,这些值应该在 LayoutModifier 之前添加
     */
    fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
        if (modifier is DrawModifier) {
            add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index) // 0
        }
        if (modifier is PointerInputModifier) {
            add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index) // 1
        }
        if (modifier is SemanticsModifier) {
            add(SemanticsEntity(layoutNodeWrapper, modifier), SemanticsEntityType.index) // 2
        }
        if (modifier is ParentDataModifier) {
            add(SimpleEntity(layoutNodeWrapper, modifier), ParentDataEntityType.index) // 3
        }
    }
    
    @kotlin.jvm.JvmInline
    value class EntityType<T : LayoutNodeEntity<T, M>, M : Modifier>(val index: Int)

    companion object {
        val DrawEntityType = EntityType<DrawEntity, DrawModifier>(0)
        val PointerInputEntityType = EntityType<PointerInputEntity, PointerInputModifier>(1)
        val SemanticsEntityType = EntityType<SemanticsEntity, SemanticsModifier>(2)
        val ParentDataEntityType =
            EntityType<SimpleEntity<ParentDataModifier>, ParentDataModifier>(3)
        val OnPlacedEntityType =
            EntityType<SimpleEntity<OnPlacedModifier>, OnPlacedModifier>(4)
        val RemeasureEntityType =
            EntityType<SimpleEntity<OnRemeasuredModifier>, OnRemeasuredModifier>(5)
        @OptIn(ExperimentalComposeUiApi::class)
        val LookaheadOnPlacedEntityType =
            EntityType<SimpleEntity<LookaheadOnPlacedModifier>, LookaheadOnPlacedModifier>(
                6
            )

        private const val TypeCount = 7
    }
}

add() 就是以头插法将新加入的 LayoutNodeEntity 添加到对应的链表头,然后再更新数组:

    private fun <T : LayoutNodeEntity<T, *>> add(entity: T, index: Int) {
        @Suppress("UNCHECKED_CAST")
        // 从 entities 数组中的 index 位置上取出链表头
        val head = entities[index] as T?
        // 头插,新加入的 entity 对象作为新的链表头
        entity.next = head
        // entities 数组更新新的链表头元素
        entities[index] = entity
    }

这样可以在 outerLayoutNodeWrapper 的结构图中,为 LayoutNodeWrapper 再增加一个 EntityList 结构:

ModifiedLayoutNode(entities = [null,null,null,null,null,null,null]
	padding(10.dp) - PaddingModifier,
	ModifiedLayoutNode(entities = [null,null,null,null,null,null,null]
        padding(20.dp) - PaddingModifier,
        innerLayoutNodeWrapper - InnerPlaceable(entities = [null,null,null,null,null,null,null])
    )
)

每个 entities 的固定位置存放的都是固定的 LayoutNodeEntity 的子类型的链表,由于我们还没有为其举例,因此暂时都是 null。

3、绘制过程

接下来看使用时设置的 DrawModifier 是如何绘制的,具体代码在 LayoutNode 的 draw() 中:

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

上一节我们回顾过,innerLayoutNodeWrapper 负责组件自身的测量与布局,而 outerLayoutNodeWrapper 负责从外部进行整个组件树的测量与布局,层级结构也通过举例具象化了,请记住这个结构,在绘制过程中有用。

下面来看 outerLayoutNodeWrapper 的 draw() 的具体内容:

    fun draw(canvas: Canvas) {
        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)
        }
    }

draw() 的绘制会根据 layer 是否为空分为两种情况。

我们先说一下 layer,它是一个独立绘制的图层,起到一个分层隔离,独立刷新的作用。它可能存在,但默认情况以及大多数时候,它是不存在的。底层实现在 Android 10(API 29)以下是用一个额外独立的 View 作为图层的绘制承载工具,从 Android 10 开始使用 RenderNode。

所以,draw() 内的绘制,会在 layer 不为空时在 layer 上绘制,当 layer 为空时在 canvas 上绘制。

3.1 layer 为空的绘制过程

我们先看默认 layer 为空的情况,核心的绘制功能在 drawContainedDrawModifiers() 中:

	private fun drawContainedDrawModifiers(canvas: Canvas) {
        val head = entities.head(EntityList.DrawEntityType)
        if (head == null) {
            performDraw(canvas)
        } else {
            head.draw(canvas)
        }
    }

head() 用于根据传入的 entityType 在 entities 数组中的 index 取出对应的 EntityType 的链表头:

	@Suppress("UNCHECKED_CAST")
    fun <T : LayoutNodeEntity<T, M>, M : Modifier> head(entityType: EntityType<T, M>): T? =
        entities[entityType.index] as T?

这里我们传参 DrawEntityType,它的 index 是 0,因此 head 就是从 entities 数组中取出的 DrawEntity 类型的链表头。

下一步根据 head 是否为空分为两种情况:

  • 如果 head 为空,说明 DrawEntity 链表为空,也就是在设置 Modifier 时没有设置过 DrawModifier,那么就用 performDraw() 绘制组件自身内容即可
  • 如果 head 不为空,证明我们在设置 Modifier 时至少设置了一个 DrawModifier,那么就从 DrawEntity 链表头开始绘制

没设置 DrawModifier 的绘制

我们先看第一种情况,不设置 DrawModifier 的情况下如何绘制:

internal abstract class LayoutNodeWrapper(
    internal val layoutNode: LayoutNode
) : LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope,
        (Canvas) -> Unit {
	internal open val wrapped: LayoutNodeWrapper? get() = null
	open fun performDraw(canvas: Canvas) {
        wrapped?.draw(canvas)
    }
}

这时候要考虑一下 LayoutNodeWrapper 的具体类型,因为我们是从 outerLayoutNodeWrapper.draw(canvas) 调用到 performDraw(),一路都是在 LayoutNodeWrapper 内,那么 outerLayoutNodeWrapper 的实际类型就决定了 wrapped 到底是什么。

前面我们回顾过,outerLayoutNodeWrapper 的初始值是 innerLayoutNodeWrapper,类型是 InnerPlaceable,那么 wrapped 就取默认值 null。但假如给 Modifier 链中配置了 LayoutModifier,那么 outerLayoutNodeWrapper 的类型就是 ModifiedLayoutNode,它在初始化时给 wrapped 传了一个 LayoutNodeWrapper:

@OptIn(ExperimentalComposeUiApi::class)
internal class ModifiedLayoutNode(
    override var wrapped: LayoutNodeWrapper, // 给 wrapped 传值了
    var modifier: LayoutModifier
) : LayoutNodeWrapper(wrapped.layoutNode)

为了明确参数中的 wrapped 到底是什么,我们还是结合实例来看:

Box(Modifier.padding(8.dp).size(40.dp))
outerLayoutNodeWrapper = 
ModifiedLayoutNode1(
	PaddingModifier,
    ModifiedLayoutNode2(
        SizeModifier,
    	InnerPlaceable
    )
)

foldOut() 遍历是从 Modifier 链的右侧开始,size() 内的 SizeModifier 是一个 LayoutModifier,因此要创建一个 ModifiedLayoutNode 将 SizeModifier 与 foldOut() 的初始值,也就是负责 Box 自身的测量的 InnerPlaceable 包装在一起,此时 wrapped 就是 InnerPlaceable。

在遍历到 padding(),其内部的 PaddingModifier 也是一个 LayoutModifier,因此也要创建一个 ModifiedLayoutNode 将 PaddingModifier 与本次遍历的初始值,也是上一次遍历的结果 ModifiedLayoutNode2 包装在一起,此时 wrapped 就是 ModifiedLayoutNode2。

结合代码,就是 ModifiedLayoutNode2 是 ModifiedLayoutNode1 的 wrapped,InnerPlaceable 是 ModifiedLayoutNode2 的 wrapped。所以现在你应该明白 performDraw() 的含义了,就是递归调用内层 LayoutNodeWrapper 的 draw(),直到最内层的 InnerPlaceable,它的 wrapped 为 null,没有下一个内层让它调用 draw()。

总结一下,在 Modifier 没有设置 DrawModifier 的情况下,绘制会从最外层的 LayoutNodeWrapper 开始,向内层逐个调用每层 LayoutNodeWrapper 的 draw(),直到最内层的 InnerPlaceable。

看到这里,不知你是否会有疑问,上面似乎没有看到原有内容的绘制,比如 Button() 的背景、Text() 的文字这些自身的内容。实际上这些绘制在内部都使用了 DrawModifier,所以在上面的代码中才没有体现,这种组件自身内容的绘制属于下一节要介绍的内容。

对于设置了 DrawModifier 的绘制,也要分两种情况来分析,DrawEntity 链表头节点与其他节点的绘制原理并不完全相同,分成两小节来分析。

DrawEntity 链表头节点的绘制

回头我们再看 drawContainedDrawModifiers() 中的第二种情况,使用了 DrawModifier 的情况,会调用 DrawEntity 链表头节点的 draw():

	// This is not thread safe
    fun draw(canvas: Canvas) {
        val size = size.toSize()
        ...

        val drawScope = layoutNode.mDrawScope
        // this 是 DrawEntiry,lambda 表达式内实际上就是调用了 DrawModifier 的 draw()
        drawScope.draw(canvas, size, layoutNodeWrapper, this) {
            with(drawScope) {
                with(modifier) {
                    draw()
                }
            }
        }
    }

调用 LayoutNodeDrawScope 的 draw(),lambda 表达式内实际上就是在调用 DrawModifier 的 draw() 进行绘制,这里还没被调用,只是封装到 lambda 表达式中作为函数参数 block 向下传递:

	internal inline fun draw(
        canvas: Canvas,
        size: Size,
        layoutNodeWrapper: LayoutNodeWrapper,
        drawEntity: DrawEntity,
        block: DrawScope.() -> Unit
    ) {
        // 临时使用参数传进来的 drawEntity 保存到 drawEntity 属性中
        val previousDrawEntity = this.drawEntity
        this.drawEntity = drawEntity
        canvasDrawScope.draw(
            layoutNodeWrapper.measureScope,
            layoutNodeWrapper.measureScope.layoutDirection,
            canvas,
            size,
            block
        )
        // 绘制完成后恢复 drawEntity 属性为原来的值
        this.drawEntity = previousDrawEntity
    }

调用 CanvasDrawScope 的 draw(),执行了参数上的 block 函数:

	inline fun draw(
        density: Density,
        layoutDirection: LayoutDirection,
        canvas: Canvas,
        size: Size,
        block: DrawScope.() -> Unit
    ) {
        // Remember the previous drawing parameters in case we are temporarily re-directing our
        // drawing to a separate Layer/RenderNode only to draw that content back into the original
        // Canvas. If there is no previous canvas that was being drawing into, this ends up
        // resetting these parameters back to defaults defensively
        val (prevDensity, prevLayoutDirection, prevCanvas, prevSize) = drawParams
        drawParams.apply {
            this.density = density
            this.layoutDirection = layoutDirection
            this.canvas = canvas
            this.size = size
        }
        canvas.save()
        // 调用了参数上的 block 
        this.block()
        canvas.restore()
        drawParams.apply {
            this.density = prevDensity
            this.layoutDirection = prevLayoutDirection
            this.canvas = prevCanvas
            this.size = prevSize
        }
    }

在这里最终执行了 DrawModifier 的 draw() 进行绘制。对于我们我们前面在基本用法中提到的 drawWithContent() 而言:

Box(
    Modifier
    .size(40.dp)
    .drawWithContent {
        drawContent()
        // 在蓝色 Box 上面画一个红色的圆
        drawCircle(Color.Red)
    }
    .background(Color.Blue)
)

drawWithContent() 最后的 lambda 表达式是它的 onDraw 参数:

fun Modifier.drawWithContent(
    onDraw: ContentDrawScope.() -> Unit
): Modifier = this.then(
    DrawWithContentModifier(
        onDraw = onDraw,
        inspectorInfo = debugInspectorInfo {
            name = "drawWithContent"
            properties["onDraw"] = onDraw
        }
    )
)

这个 onDraw 是在 DrawWithContentModifier 实现 DrawModifier 接口的 ContentDrawScope.draw() 时被调用的:

private class DrawWithContentModifier(
    val onDraw: ContentDrawScope.() -> Unit,
    inspectorInfo: InspectorInfo.() -> Unit
) : DrawModifier, InspectorValueInfo(inspectorInfo) {

    override fun ContentDrawScope.draw() {
        onDraw()
    }
}

因此,绘制的内容就是 drawWithContent() 最后的 lambda 表达式内的内容。

DrawEntity 链表其他节点的绘制

上面只讲了头节点 head 的绘制,但还要考虑 Modifier 链上其他的 DrawModifier 是如何绘制的。

我们讲用法时说过,需要在 () 内调用的 drawContent() 才能绘制原有内容,否则就只会绘制当前这个 drawWithContent() 内所指定的绘制内容。这是因为,DrawEntity 链表其他节点的绘制正是通过 drawContent() 实现的。

drawContent() 是 ContentDrawScope 接口的函数,实现类只有一个 LayoutNodeDrawScope:

internal class LayoutNodeDrawScope(
    private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
) : DrawScope by canvasDrawScope, ContentDrawScope {

    private var drawEntity: DrawEntity? = null

    override fun drawContent() {
        drawIntoCanvas { canvas ->
            val drawEntity = drawEntity!!
            val nextDrawEntity = drawEntity.next
            if (nextDrawEntity != null) {
                nextDrawEntity.draw(canvas)
            } else {
                drawEntity.layoutNodeWrapper.performDraw(canvas)
            }
        }
    }
}

drawIntoCanvas() 只是接收了一个 Canvas 参数然后直接调用了它的 lambda 表达式参数 block:

inline fun DrawScope.drawIntoCanvas(block: (Canvas) -> Unit) = block(drawContext.canvas)

block 内会取 drawEntity 的下一个节点 nextDrawEntity,如果为空,就调用 drawEntity 所在的 LayoutNodeWrapper 的 performDraw(),这个我们前面已经看过了:

	open fun performDraw(canvas: Canvas) {
        // 让当前 LayoutNodeWrapper 内包装的 LayoutNodeWrapper 开始执行,
        // 如果到了最内层的 InnerPlaceable,wrapped 为 null 就不绘制
        wrapped?.draw(canvas)
    }

如果 nextDrawEntity 不为空,则执行 nextDrawEntity 的 draw():

	// This is not thread safe
    fun draw(canvas: Canvas) {
        val size = size.toSize()
        if (cacheDrawModifier != null && invalidateCache) {
            layoutNode.requireOwner().snapshotObserver.observeReads(
                this,
                onCommitAffectingDrawEntity,
                updateCache
            )
        }

        val drawScope = layoutNode.mDrawScope
        drawScope.draw(canvas, size, layoutNodeWrapper, this) {
            with(drawScope) {
                with(modifier) {
                    draw()
                }
            }
        }
    }

让下一个 DrawEntity 去绘制。这个代码过程在上一小节讲 DrawEntity 链表头节点的绘制时已经说过,最终会根据 DrawModifier 指定的绘制内容去进行绘制。

所以,想要绘制 Modifier 链中 DrawEntity 链表的非头节点,你必须调用 drawContent(),它会取 DrawEntity 链表的下一个节点,如果节点非空则让该节点进行绘制,否则就交给该节点所在的 LayoutNodeWrapper,让它触发它内部包含的 LayoutNodeWrapper 的绘制流程。

本小节的最后,我们也再次强调一下,为了让绘制链条不中断,一定记得要在定义 DrawModifier 时调用 drawContent()。

总结

最后我们还是结合实例来帮助理解代码,还是之前的例子:

Box(Modifier.padding(8.dp).size(40.dp))

outerLayoutNodeWrapper = 
ModifiedLayoutNode1(entities = [DrawModifier1 -> DrawModifier2,null,null,null,null,null,null]
	padding(10.dp) - PaddingModifier,
	ModifiedLayoutNode2(entities = [DrawModifier3,null,null,null,null,null,null]
        padding(20.dp) - PaddingModifier,
        innerLayoutNodeWrapper - InnerPlaceable(
            entities = [DrawModifier4 -> DrawModifier5 -> DrawModifier6,null,null,null,null,null,null]
        )
    )
)

每个 LayoutNodeWrapper 中的 entities 数组中的 DrawModifier 是伪造的,要不写在 Box() 的示例代码中会很占篇幅。

整体的绘制流程是:

  • LayoutNode 的 draw() 调用 outerLayoutNodeWrapper,也就是 LayoutNodeWrapper,在上面的层级图中的 ModifiedLayoutNode1 的 draw() 开始绘制
  • ModifiedLayoutNode1 看自己的 entities 的 DrawEntity 链表,调用表头,也就是 DrawModifier1 的 draw() 绘制表头中的内容
  • 表头绘制时需调用 drawContent(),它会执行下一个节点 DrawModifier2 的 draw()
  • DrawModifier2 的 draw() 内也需调用 drawContent(),这样才会在没有下一个节点的情况下,让 ModifiedLayoutNode1 调用它的 wrapped 属性,也就是 ModifiedLayoutNode2 的 draw()
  • ModifiedLayoutNode2 也是一个 LayoutNodeWrapper,调用它的 draw() 的过程就是第一步到上一步的过程,如出一辙,就是 ModifiedLayoutNode2 找它的 DrawEntity 链表进行绘制,链表尾的 DrawEntity 通过内部调用的 drawContent() 找到 ModifiedLayoutNode2 包含的下一个 LayoutNodeWrapper —— innerLayoutNodeWrapper
  • innerLayoutNodeWrapper 还是同样的找 DrawEntity 链表,按照顺序绘制 DrawModifier4、DrawModifier5、DrawModifier6,在 DrawModifier6 的 drawContent() 内,由于没有后续节点了,就会执行 innerLayoutNodeWrapper 的 performDraw(),由于 innerLayoutNodeWrapper 的类型 InnerPlaceable 没有为 wrapped 属性赋值,因此就采用父类中的默认值 null,这样 performDraw() 内的 wrapped?.draw() 就不会执行,绘制结束

将上述过程形成伪代码结构,假如每个 DrawModifier 内都是先绘制自己的内容后再调用 drawContent(),就是如下的包裹关系:

DrawModifier1.draw() {
	// 先绘制 DrawModifier1 自身内容...
	drawContent() {
		DrawModifier2.draw() {
		   // 先绘制 DrawModifier2 自身内容...
            drawContent() {
		   		DrawModifier3.draw() {
		   			...
		   		}
            }
        }
	}
}

但假如每个 DrawModifier 都是先调用 drawContent() 再绘制自己的内容,包裹关系就变成:

DrawModifier1.draw() {
	drawContent() {
		DrawModifier2.draw() {
            drawContent() {
		   		DrawModifier3.draw() {
		   			...
		   		}
            }
            // 后绘制 DrawModifier2 自身内容...
        }
	}
	// 后绘制 DrawModifier1 自身内容...
}

3.2 layer 不为空的绘制过程

layer 这个知识有点偏,因此我们就只简单说说。

回到 LayoutNodeWrapper 的 draw() 中,当 layer 不为空时,调用 layer 的 drawLayer():

	fun draw(canvas: Canvas) {
        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)
        }
    }

这个 layer 是 OwnedLayer 接口的实例,该接口有两个实现类 ViewLayer 与 RenderNodeLayer,我们只看后者的实现:

	override fun drawLayer(canvas: Canvas) {
        val androidCanvas = canvas.nativeCanvas
        if (androidCanvas.isHardwareAccelerated) {
            ...
        } else {
            ...
            drawBlock?.invoke(canvas)
            ...
        }
    }

关键的执行绘制的语句就是调用 drawBlock 函数,它是 RenderNodeLayer 内的属性:

private var drawBlock: ((Canvas) -> Unit)? = drawBlock

这个函数的赋值是在 onLayerBlockUpdated() 中,调用 createLayer() 创建 layer 时:

	fun onLayerBlockUpdated(layerBlock: (GraphicsLayerScope.() -> Unit)?) {
        val layerInvalidated = this.layerBlock !== layerBlock || layerDensity != layoutNode
            .density || layerLayoutDirection != layoutNode.layoutDirection
        this.layerBlock = layerBlock
        this.layerDensity = layoutNode.density
        this.layerLayoutDirection = layoutNode.layoutDirection
        if (isAttached && layerBlock != null) {
            if (layer == null) {
                layer = layoutNode.requireOwner().createLayer(
                    this,
                    invalidateParentLayer
                ).apply {
                    resize(measuredSize)
                    move(position)
                }
                updateLayerParameters()
                layoutNode.innerLayerWrapperIsDirty = true
                invalidateParentLayer()
            } else if (layerInvalidated) {
                updateLayerParameters()
            }
        } else {
            ...
        }
    }

createLayer() 是 Owner 接口的函数,实现是在 AndroidComposeView 中,第一个参数就是 drawBlock:

	override fun createLayer(
        drawBlock: (Canvas) -> Unit,
        invalidateParentLayer: () -> Unit
    ): OwnedLayer {
        ...
    }

而 onLayerBlockUpdated() 中给这个 drawBlock 传的是 this,实际就指向了 invoke():

    @Suppress("LiftReturnOrAssignment")
    override fun invoke(canvas: Canvas) {
        if (layoutNode.isPlaced) {
            snapshotObserver.observeReads(this, onCommitAffectingLayer) {
                drawContainedDrawModifiers(canvas)
            }
            lastLayerDrawingWasSkipped = false
        } else {
            // The invalidation is requested even for nodes which are not placed. As we are not
            // going to display them we skip the drawing. It is safe to just draw nothing as the
            // layer will be invalidated again when the node will be finally placed.
            lastLayerDrawingWasSkipped = true
        }
    }

invoke() 内会调用 drawContainedDrawModifiers():

	private fun drawContainedDrawModifiers(canvas: Canvas) {
        val head = entities.head(EntityList.DrawEntityType)
        if (head == null) {
            performDraw(canvas)
        } else {
            head.draw(canvas)
        }
    }

这个函数你应该很熟悉了,前面讲 layer 为空的绘制流程时,需要让 LayoutNodeWrapper 进行绘制时,就是调用的这个函数。

因此,layer 不为空实际上也是通过 LayoutNodeWrapper 的 drawContainedDrawModifiers() 进行的绘制。

4、举例与总结

虽然在讲解原理的过程中,我们为了更好地理解源码,已经举了一些小例子。但是,原理已经讲完,我们再结合一些实际的例子来验证一下源码中总结出的结论。

首先,判断如下 Box 的背景颜色:

Box(Modifier.size(40.dp).background(Color.Red).background(Color.Blue))

答案是蓝色。因为 LayoutNode 的 modifier 属性的 set() 内,通过 foldOut() 计算 layoutDelegate.outerWrapper,也就是 outerLayoutNodeWrapper。按照 foldOut() 从右至左的遍历顺序,先遍历到 Blue,然后才遍历到 Red,但因为链表是头插法,后遍历到的会在链表头,因此 Red 位于链表头,Blue 在它后面,所以最终会形成如下的结果:

outerLayoutNodeWrapper = 
ModifiedLayoutNode[
	SizeModifier,
	InnerPlaceable(entities = [Background(Red) -> Background(Blue),null,null,null,null,null,null])
]

在绘制时,按照链表从头到尾的顺序绘制,因此先绘制 Red,再绘制 Blue,后绘制的会覆盖前面的,因此 Box 的颜色是蓝色。也就是说,相同的绘制属性,靠右侧的 Modifier生效。

再看第二个例子,这是本篇文章开篇提到的问题,判断如下 Box 的蓝色区域的尺寸是多少:

Box(Modifier.requiredSize(80.dp).background(Color.Blue).requiredSize(40.dp))

答案是 40,它形成的是一个 80 的 Box,但是居中的蓝色方块是 40:

在这里插入图片描述

这是因为 Background 这个 DrawModifier,在 foldOut() 遍历时,是被添加到当前本次遍历的初始值 toWrap 中:

toWrap.entities.addBeforeLayoutModifier(toWrap, mod)

我们前面说过,这个 toWrap 初始是 InnerPlaceable,遇到 LayoutModifier 后会由 ModifiedLayoutNode 包住上一次遍历的 toWrap。所以像第一个例子中,全是 padding() 的情况,两个 PaddingModifier 就会被添加到初始的 InnerPlaceable 中。而这个例子中,右边的 requiredSize() 形成第一个 ModifiedLayoutNode 包住 InnerPlaceable,然后 background() 将 Background 添加到 ModifiedLayoutNode 的 DrawModifier 链表中,所以蓝色背景的尺寸是 40dp。最后遍历到左侧的 requiredSize(),生成第二个 ModifiedLayoutNode 包住第一个 ModifiedLayoutNode,使得 Box 的整体尺寸为 80dp,但居中摆放了一个 40dp 的蓝色背景:

outerLayoutNodeWrapper = 
ModifiedLayoutNode(entities = [null,null,null,null,null,null,null]
	ModifiedLayoutNode([entities = [Background,null,null,null,null,null,null]
        SizeModifier(40.dp),
        InnerPlaceable(entities = [null,null,null,null,null,null,null])
    )
)

也就是说,DrawModifier 会被添加到与它相邻的最近的那个 LayoutModifier 表示的 LayoutNodeWrapper 中。比如在现有基础上再加一个 background():

Box(Modifier.background(Color.Red).requiredSize(80.dp).background(Color.Blue).requiredSize(40.dp))
在这里插入图片描述

红色的 Background 会被添加到 80 的 ModifiedLayoutNode 中。

最后一个例子,下面 Box 的蓝色背景的尺寸是多大,如何计算的:

Box(Modifier.size(40.dp).padding(8.dp).background(Color.Blue))

答案是 24,计算方式是,从右向左,先让 InnerPlaceable 的背景是蓝色的,然后让 padding 是 8dp,最后是 Box 的尺寸 40dp,这样让 InnerPlaceable 测量出的尺寸为 24,四周有 8dp 的内边距。这个结果会给人造成一种,尺寸是从左向右计算得出的错觉,假如你把这种错觉应用到两个方向不同结果的情况时,就会得出错误的结果,比如:

Box(Modifier.size(40.dp).padding(8.dp).background(Color.Blue).requiredSize(34.dp))

如果按照正确的从右向左算,会得到一个 40dp 的 Box,居中摆着 34dp 的蓝色方块,四个方向内边距为 3dp;而如果按照错觉的从左向右计算,会得到一个 34dp 的蓝色 Box。

总结:

  1. 从绘制关系上看,DrawModifier 是从左到右的包裹关系(数据结构关系是链表,最左侧的 DrawModifier 是链表头),需要通过手动 drawContent() 才能确保所有 DrawModifier 都得以绘制,否则绘制链条会断掉
  2. DrawModifier 的尺寸,由距他最近的 LayoutModifier 决定

http://www.kler.cn/a/587207.html

相关文章:

  • matplotlib与numpy版本不兼容问题
  • OpenFeign的配置类可以进行哪些配置
  • 用jupyter写csdn文章
  • 2.5 滑动窗口专题:水果成篮
  • 996引擎-问题处理:缺失特效分割文件 ModelAtlasSplitConfigs
  • MAC安装logisim教程(新手级详细教程)
  • 机器学习——正则化、欠拟合、过拟合、学习曲线
  • 智能语音对话小程序功能优化day2-语音输入
  • TensorFlow 是什么?
  • 界面组件DevExpress WPF中文教程:Grid - 如何显示嵌套栏(Bands)?
  • 蓝桥杯 17110抓娃娃
  • 【SpringMVC】常用注解:@CookieValue
  • synchronized与 Java内置锁(未写完)
  • 《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
  • 《Electron 学习之旅:从入门到实践》
  • RK3588 openssl-3.4.1 编译安装
  • unity生命周期
  • vue埋点
  • Python实现NOA星雀优化算法优化随机森林分类模型项目实战
  • 前端工程化之前端工程化详解 包管理工具