本篇讲解 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 {// 业务绘制 1drawContent()// 业务绘制 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 = Modifierset(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.outerWrapperlayoutDelegate.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() 的第二个参数 innerLayoutNodeWrapperget() = 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.JvmInlinevalue 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 = layerif (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() = nullopen 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 safefun 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.drawEntitythis.drawEntity = drawEntitycanvasDrawScope.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 defensivelyval (prevDensity, prevLayoutDirection, prevCanvas, prevSize) = drawParamsdrawParams.apply {this.density = densitythis.layoutDirection = layoutDirectionthis.canvas = canvasthis.size = size}canvas.save()// 调用了参数上的 block this.block()canvas.restore()drawParams.apply {this.density = prevDensitythis.layoutDirection = prevLayoutDirectionthis.canvas = prevCanvasthis.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? = nulloverride fun drawContent() {drawIntoCanvas { canvas ->val drawEntity = drawEntity!!val nextDrawEntity = drawEntity.nextif (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 safefun draw(canvas: Canvas) {val size = size.toSize()if (cacheDrawModifier != null && invalidateCache) {layoutNode.requireOwner().snapshotObserver.observeReads(this,onCommitAffectingDrawEntity,updateCache)}val drawScope = layoutNode.mDrawScopedrawScope.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 = layerif (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.nativeCanvasif (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.layoutDirectionthis.layerBlock = layerBlockthis.layerDensity = layoutNode.densitythis.layerLayoutDirection = layoutNode.layoutDirectionif (isAttached && layerBlock != null) {if (layer == null) {layer = layoutNode.requireOwner().createLayer(this,invalidateParentLayer).apply {resize(measuredSize)move(position)}updateLayerParameters()layoutNode.innerLayerWrapperIsDirty = trueinvalidateParentLayer()} 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。
总结:
- 从绘制关系上看,DrawModifier 是从左到右的包裹关系(数据结构关系是链表,最左侧的 DrawModifier 是链表头),需要通过手动 drawContent() 才能确保所有 DrawModifier 都得以绘制,否则绘制链条会断掉
- DrawModifier 的尺寸,由距他最近的 LayoutModifier 决定