加强机关网站内容建设/全达seo

加强机关网站内容建设,全达seo,网站备案起名要求,贸易公司网站设计本篇讲解 DrawModifier 的基本用法与代码原理,介绍原理的目的在于可以判断绘制与绘制的关系,绘制与布局的关系。知道达成某种绘制效果应该怎么写,面对复杂的 Modifier 链时对效果有大致预判。 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 {// 业务绘制 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。

总结:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/72434.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华为手机助手输入连接码时光标乱跳

问题复现&#xff1a;输入12345678&#xff0c;光标自动跳转导致连接码出现乱序情况。 千万别试着找出规律&#xff0c;已试动态规律非大牛误轻试 问题原因&#xff1a; 想啥呢&#xff1f;华哥的软件又不是我开发我要Know Why干啥 我只需关心解决方案 &#xff08;可能时输入…

Windows 11 安装Docker Desktop环境

1、确认CPU开启虚拟化 打开任务管理器&#xff0c;切换到“性能”选项卡&#xff0c;查看 CPU 信息。若“虚拟化”状态显示为“已启用”&#xff0c;则表示虚拟化已开启&#xff1b;若显示为“已禁用”&#xff0c;则需要在启动时进入 BIOS 开启虚拟化设置&#xff08;若显示已…

STM32如何精准控制步进电机?

在工业自动化、机器人控制等场合&#xff0c;步进电机以其高精度、开环控制的特性得到了广泛应用。而在嵌入式系统中&#xff0c;使用STM32进行步进电机的精确控制&#xff0c;已成为开发者的首选方案之一。 本文将从嵌入式开发者的角度&#xff0c;深入探讨如何基于STM32 MCU…

【 <一> 炼丹初探:JavaWeb 的起源与基础】之 JavaWeb 项目的部署:从开发环境到生产环境

<前文回顾> 点击此处查看 合集 https://blog.csdn.net/foyodesigner/category_12907601.html?fromshareblogcolumn&sharetypeblogcolumn&sharerId12907601&sharereferPC&sharesourceFoyoDesigner&sharefromfrom_link <今日更新> 一、开发环境…

算法题(98):大数加法

审题&#xff1a; 本题需要我们解决大数加法&#xff0c;大数直接运算会超出范围&#xff0c;所以我们需要转换成字符串一位位进行计算 思路&#xff1a; 方法一&#xff1a;高精度加法 我们将两个大数的每一个位分别计算&#xff0c;然后头插到answer字符串中即可 解题&#x…

C# Exe + Web 自动化 (BitComet 绿灯 自动化配置、设置)

BitComet GreenLight,内网黄灯转绿灯 (HighID), 增加p2p连接率提速下载-CSDN博客 前两天写个这个&#xff0c;每次开机关机后要重来一遍很麻烦的索性写个自动化。 先还是按照上面的教程自己制作一遍&#xff0c;留下Luck 以及 路由器相关的 端口记录信息。 &#xff08;因为自…

python---序列 (str,list,tuple)

一、 序列类型入门 python的数据类型&#xff1a;int float bool str 运算符 - * / % > < and or not 流程控制ifelsewhilefor掌握python的2大容器类型数值类型&#xff08;3个&#xff09;&#xff1a;int float bool序列类型容器(3个)&#xff1a;str &#xff1a; …

CSS元素层叠顺序规则

CSS元素层叠顺序规则 看图说话总结: background/borderz-index(<0)blockfloatinline/inline-blockz-index(0,auto)z-index (>0)

ArcGIS Pro将有文字标注底图切换为无标注底图(在线地图图源)

今天介绍一下在ArcGIS Pro将有标注的地形底图换成无标注的底图。 大家在这项目底图时候会经常调用ArcGIS Pro自带的地形图&#xff0c;但是这个地形图自带是有注记的&#xff0c;如下图。 如何更改&#xff0c;才可以调用无文字注记的呢&#xff1f; 对于一个已经切好图的有注记…

Xxl-Job学习笔记

目录 概述 核心架构 核心特点 应用场景 什么是任务调度 快速入门 获取源码 初始化调度数据库 基本配置 数据源datasource 邮箱email&#xff08;可选&#xff09; 会话令牌access token 启动调度中心 启动执行器 依赖 yaml基本配置 XxlJobConfig类配置 定义执…

让双向链表不在云里雾里

又来博客留下我的足迹了&#xff0c;哈哈哈&#xff0c;这次是对于双向链表的理解 目录 创建双向链表&#xff1a; 申请结点&#xff1a; 双向链表初始化&#xff1a; 双向链表插入结点&#xff1a; 双向链表删除结点&#xff1a; 双向链表的打印&#xff1a; 双向链表…

前端工程化之前端工程化详解 包管理工具

前端工程化详解 & 包管理工具 前端工程化什么是前端工程化前端工程化发展脚手架能力 体验度量规范流程效能流程扭转 稳定性建设针对整体稳定性建设 可监控&#xff1a;前端监控系统 包管理工具npm包详解package.jsonname 模块名description 模块描述信息keywords&#xff1…

《Python实战进阶》No24: PyAutoGUI 实现桌面自动化

No24: PyAutoGUI 实现桌面自动化 摘要 PyAutoGUI 是一个跨平台的桌面自动化工具&#xff0c;能够模拟鼠标点击、键盘输入、屏幕截图与图像识别&#xff0c;适用于重复性桌面任务&#xff08;如表单填写、游戏操作、批量文件处理&#xff09;。本集通过代码截图输出日志的实战形…

一周学会Flask3 Python Web开发-SQLAlchemy查询所有数据操作-班级模块

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 我们来新建一个的蓝图模块-班级模块&#xff0c;后面可以和学生模块&#xff0c;实现一对多的数据库操作。 blueprint下新建g…

Neural Architecture Search for Transformers:A Survey

摘要 基于 Transformer 的深度神经网络架构因其在自然语言处理 (NLP) 和计算机视觉 (CV) 领域的各种应用中的有效性而引起了极大的兴趣。这些模型是多种语言任务&#xff08;例如情绪分析和文本摘要&#xff09;的实际选择&#xff0c;取代了长短期记忆 (LSTM) 模型。视觉 Tr…

TCP 全连接队列 内核层理解socket

TCP 全连接队列 理解 listen 的第二个参数 int listen(int sockfd, int backlog);backlog 参数表示 全连接队列&#xff08;accept 队列&#xff09;的最大长度。 那什么是全连接队列呢&#xff1f; 三次握手 & accept() 处理流程 客户端发送 SYN&#xff0c;服务器收到并…

OpenEuler-22.03-LTS上利用Ansible轻松部署MySQL 5.7

一、需求 使用ansible自动化部署mysql二进制部署mysql部署mysql并创建JDBC用户 二、环境信息 本文涉及的代码&#xff0c;配置文件地址&#xff1a; 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;1g6y 软件名称版本备注Ansible2.9.27All modules — Ansible Doc…

基于javaweb的SpringBoot农资商城购物商城系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…

angular打地鼠

说明&#xff1a;我计划用angular做一款打地鼠的小游戏&#xff0c; 打地鼠游戏实现文档 &#x1f3ae; 游戏逻辑 ​游戏场景 采用 3x3 网格布局的 9 个地鼠洞​核心机制 地鼠随机从洞口弹出点击有效目标获得积分30 秒倒计时游戏模式 ​难度系统 简单模式&#xff1a;生成间…

博客网站(springboot)整合deepseek实现在线调用

&#x1f389;&#x1f389;&#x1f389;&#x1f389;&#x1f389;&#x1f389; 欢迎访问的个人博客&#xff1a;https://swzbk.site/&#xff0c;加好友&#xff0c;拉你入福利群 &#x1f389;&#x1f389;&#x1f389;&#x1f389;&#x1f389;&#x1f389; 1、de…