前言
在上一篇文章中,介绍了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
类型,例如LayoutModifier
、DrawModifier
这种,就会执行其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中的内容不会被绘制。
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.Node
是 DrawModifierNode
类型,那么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
函数,如果在LayoutNode
的NodeChain
中,没有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,如果拿到了,那么就会正常执行其内部的绘制操作,performDraw
是DrawModifierNode
的一个扩展函数,它会执行组件的内部绘制操作。
// 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
下,整体的包裹效果如下所示:
那么在测量的时候,从外层开始测量,最大值为60dp,那么会画一个60dp的绿色背景;然后带着60dp的约束条件给到下一个节点LayoutModifierNodeCoordinator
进行测量,因此会判断如果大于60dp,那么就会最大限制为60dp(伙伴们可以试下),如果小于60dp,那么就是按照实际的值测量,然后绘制。
通过上面的例子,我们得到了一个结论,就是右侧的LayoutModifier
决定绘制区域的大小,那么再看下的例子:
@Composable
fun ModifierDraw() {
Text(text = "测试",Modifier.size(200.dp).background(Color.Green))
}
最终显示的是一个200dp的Text,嗯?左边的LayoutModifier
也会决定绘制的区域大小,其实这个只是一个错觉,因为在同步的时候,background
是包裹在InnerCoordinator
中的,也就是组件自身。而size
作为LayoutModifier决定了组件的大小为200dp,因为在绘制的时候,也就绘制了200dp的区域。