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 {// 業務繪制 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.pswp.cn/pingmian/72434.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/72434.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/72434.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

華為手機助手輸入連接碼時光標亂跳

問題復現&#xff1a;輸入12345678&#xff0c;光標自動跳轉導致連接碼出現亂序情況。 千萬別試著找出規律&#xff0c;已試動態規律非大牛誤輕試 問題原因&#xff1a; 想啥呢&#xff1f;華哥的軟件又不是我開發我要Know Why干啥 我只需關心解決方案 &#xff08;可能時輸入…

《DeepSeek 開源 DeepGEMM:開啟AI計算新時代的密鑰》:此文為AI自動生成

《DeepSeek 開源 DeepGEMM&#xff1a;開啟AI計算新時代的密鑰》&#xff1a;此文為AI自動生成 引言&#xff1a;AI 計算的新曙光 在當今科技飛速發展的時代&#xff0c;人工智能&#xff08;AI&#xff09;無疑是最為耀眼的領域之一。從語音助手到自動駕駛&#xff0c;從圖像…

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 <今日更新> 一、開發環境…

深度學習CNN特征提取與匹配

?一、CNN特征提取核心方法? ?基礎網絡架構? 使用卷積神經網絡&#xff08;如ResNet、VGG、MobileNet&#xff09;提取多層特征圖&#xff0c;通過卷積層自動學習圖像的局部紋理、形狀及語義信息?。 ?淺層特征?&#xff1a;邊緣、角點等低級特征&#xff08;Conv1-3&…

Vuex 高級技巧與最佳實踐

使用 map 輔助函數簡化代碼&#xff1a; javascript import { mapState, mapGetters } from vuexexport default {computed: {...mapState([num]),...mapGetters([doubleNum])} }模塊化開發&#xff1a; javascript // modules/student.js export default {namespaced: true,st…

算法題(98):大數加法

審題&#xff1a; 本題需要我們解決大數加法&#xff0c;大數直接運算會超出范圍&#xff0c;所以我們需要轉換成字符串一位位進行計算 思路&#xff1a; 方法一&#xff1a;高精度加法 我們將兩個大數的每一個位分別計算&#xff0c;然后頭插到answer字符串中即可 解題&#x…

C# Exe + Web 自動化 (BitComet 綠燈 自動化配置、設置)

BitComet GreenLight,內網黃燈轉綠燈 (HighID), 增加p2p連接率提速下載-CSDN博客 前兩天寫個這個&#xff0c;每次開機關機后要重來一遍很麻煩的索性寫個自動化。 先還是按照上面的教程自己制作一遍&#xff0c;留下Luck 以及 路由器相關的 端口記錄信息。 &#xff08;因為自…

基于 Docker 搭建 FRP 內網穿透開源項目

有些配置項不知道該不該用,不知道該在哪用,不知道怎么用,所以我自己寫個文章簡單記錄一下做個筆記 本文介紹的是基于 Docker 運行 frps 和 frpc,并通過 TCP 協議簡單穿透 SSH 和 HTTP,在觀看本文之前請確保你的機器已經安裝 Docker 服務端搭建 frps# 連接擁有公網 IP 的…

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)

刪除有序數組中的重復項(26)

26. 刪除有序數組中的重復項 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; class Solution { public:int removeDuplicates(vector<int>& nums) {auto first nums.begin();auto last nums.end();auto result first;if (first last) {return std::…

Vue 概念、歷史、發展和Vue簡介

一、Vue概念 官方定義&#xff1a; 漸進式JavaScript 框架&#xff0c;易學易用&#xff0c;性能出色&#xff0c;適用場景豐富的 Web 前端框架。 Vue.js 是一個流行的前端JavaScript框架&#xff0c;由尤雨溪&#xff08;Evan You&#xff09;開發并維護。 它最初于2014年發…

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; 雙向鏈表…

java虛擬機(JVM)以及各種參數詳解

Java 虛擬機&#xff08;JVM&#xff09;提供了許多參數來調整其行為和性能&#xff0c;以便更好地適應不同的應用場景。理解和使用這些參數對于優化 Java 應用程序的性能非常重要。以下是一些常用的 JVM 參數及其詳細說明&#xff1a; 1. 內存管理參數 -Xms<size>&…

如何搭配 AI 量化策略選股

AI 量化選股策略結合了 技術指標、基本面數據、市場情緒&#xff0c;利用 機器學習、深度學習、因子分析 等方法&#xff0c;提高選股精準度和交易決策效率。下面介紹 如何搭配 AI 量化策略選股。 1. AI 量化選股的核心方法 AI 量化選股主要依靠 數據驅動&#xff0c;包括&…

Python 爬蟲:一文掌握 SVG 映射反爬蟲

更多內容請見: 爬蟲和逆向教程-專欄介紹和目錄 文章目錄 1. SVG 概述1.1 SVG的優點1.1 映射反爬蟲的原理2. SVG 映射反爬蟲的示例3. 應對 SVG 映射反爬蟲的方法3.1 解析 SVG 圖像3.2 處理自定義字體3.3 使用 OCR 技術3.4 動態生成 SVG 的處理4. 實戰案例4.1 使用 SVG 映射顯示…