本篇講解 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 決定