ということを納得するためにJetpackComposeのCanvas周りのコードリーディングをしました。
その備忘録です。
背景 (長いので読まなくていいです。)
2022/10にあったDroidKaigiでJetpackComposeのCanvasをテーマに登壇しました。
その際に、「画像を描写した時のBlendModeの挙動が定義と異なっているように見える部分がある」、「何もかもが不明」、「何かご存知の方がいらっしゃったら教えて欲しい」としてあった部分がありました。
そこについて、解決法を教えてくださった方があり、なので登壇資料をアップデートしようかなと色々試していたら、そもそも件のBlendModeの動きはAndroidViewのCanvasでも同じように振る舞うことが判明しました。そして何となく原因も推定できました。
そこからさらにJetpackComposeの方のComposeライブラリのコードを読んでいるとなんとなく「この挙動は根本的にはAndroidViewのCanvasの挙動がこうだからこうなっているのだな。全てはAndroidViewのCanvasの動きに依存しているのだな」ということが察せられてきました。
が、とはいえ簡単には資料で言い切れるほどの自信が持てませんでした。
最終的には自信を持てるところまでコードリーディングをしたのですが、結果、すごく時間がかかったし、(資料の要旨がぼやけるので)スライドに載せるべき分量ではなくなるし、だったので自分向け備忘録および資料をみたときになんでそう言い切っているのか気になった方がいた場合のためにQiitaにまとめて公開している次第です。
環境
Compose v1.2.0
Kotlin v1.7.0
※DroidKaigi資料に合わせているのでちょっと古めです。
コードリーディング内容
非常に長いので、以下のように5段階にわけてみました。
⓪AndroidView時代のViewで画像を描写するときの復習
①ComposeのCanvasは、onDrawで渡したものもModifierになる
②CanvasのonDrawとModifierから作られたDrawBackgroundModifeirがどこでどういう風に使われていくか見ていく
③ComponentActivity.setContentを呼んだときの処理の流れを見ていく。それにより②で提示した疑問に答えを出す。
④CanvasのonDraw部分が実行されるときのDrawScopeが何かを得る
⑤CanvasのonDrawの中でdrawImageを呼んだときは巡り巡ってAndroidViewのCanvasのdrawBitmapが呼ばれている
まず、確認として、ComposeのCanvasで画像を描写するときの方法を簡単に載せます。(詳しくは、冒頭に紹介したDroidKaigiのスライドの前半部分に書かれています。)
以下はスライドにもスクリーンショットを載せていた、ComposeのCanvasにonDrawとして渡しているDrawScope.()->Unitラムダの一部です。画像を描写するためにDrawScope#drawImageメソッドを呼んでいます。(BlendModeも指定しています。)
これはつまり、ComposeのCanvasで画像を描写するときの実際の処理は、この onDrawラムダを呼び出してくるDrawScopeを実装したインスタンスがどんなクラスで、どんなdrawImageの実装を持つかに依存するということです。(DrawScopeはインターフェイスです。)

⓪AndroidView時代のViewで画像を描写するときの復習
-
View#onDrawの引数で渡ってくるandroid.graphics.CanvasのdrawBitmapメソッドを呼びます。 - この時に関しても、(Composeの
Canvasの時と同じく)背景透過画像の上からDstOverで他の画像を描き込んだ時は背景が上手く透過しない = AndroidView時代から、背景透過画像の上からDstOverで他の画像を描き込んだ時はBlendModeが定義通りには動いていないらしい - どうやらこれはハードウェア アクセラレーションのせいらしい(推測)
※詳しくはDroidKaigi資料の110ページ目〜に譲ります。
①ComposeのCanvasは、onDrawで渡したものもModifierになる
以下がComposeのCanvasの定義です。onDrawに渡したDrawScope.()->Unitラムダがmodifierのメソッドに渡され、(それと合成されて。詳しくは後述。)SpacerメソッドのModifierとして渡されていることがわかります。
ここでmodifier.drawBehind(onDraw)の処理の中身を見てみます。こんな感じです。
onDrawはDrawBackgroundModifierとなり、Canvasに渡されたmodifierのthenメソッドの引数として利用されています。Modifier#thenメソッドの詳細な内部実装は当然Modifierによって違うと思いますが、「自分と他のModifierを連結する」メソッドになります。つまり、onDrawから作ったDrawBackgroundModifierは、Canvasに元々渡されたModifierに連結されてひとつのModifierになるわけです。
ここで、DrawBackgroundModifierの定義を見てみます。

Canvasに渡されたonDrawは、、DrawBackgroundModifierのContentDrawScope.drawメソッドの中で呼び出されるようです。
つまり、「CanvasのonDrawラムダを呼び出してくるDrawScopeを実装したインスタンスがどんなクラスで、どんなdrawImageの実装を持つか」を知りたかったわけですが、それは 「設定されているModifierのContentDrawScope.drawメソッドを呼び出すのがどんなContentDrawScopeインスタンスなのか」を探っていくことに帰着する ことがわかりました。(ちなみにContentDrawScopはDrawScope`を継承したインターフェースです。)
②CanvasのonDrawとModifierから作られたDrawBackgroundModifeirがどこでどういう風に使われていくか見ていく。
さて、ここでちょっと視点を一階層上に戻して、DrawScope.()->UnitラムダがModifierとなった後にSpacerに渡され、以降引き渡されていくかを見ていきたいと思います。
さっきも出てきましたが、Spacerの定義は以下です。そしてその中の唯一の処理になっているLayoutの定義は以下です。
(Canvasに渡したmodifierとonDrawから作成したModifierが連結された)modifierは、materializerOfメソッドに渡され、その戻りがReusableComposeNode<ComposeUiNode, Applier<Any>>メソッドのskippableUpdateとして使われています。
materializerOfメソッドの実装は以下です。
最初にmodifierはcurrentComposer.materializeメソッドに渡され、戻りがupdateメソッドに渡されるラムダの中のsetメソッド(これはより細かくいうとUpdater<ComposeUiNode>#setメソッドです。)の引数に使われています。
実は、具体的にcurrentComposerを誰がセットしているのか、結局最後までわかりませんでした。が、Composerインターフェースはsealedインターフェイスであり、実装されているクラスはComposerImplしかないので、currentComposer#materializeの実装は特定できるのでとりあえず先に進めます。
currentComposer#materializeの実装は以下です。

長いですが、Canvasに渡したmodifierがたとえば極めて単純なModifier.fillMaxSize()だった場合、ここに渡ってくるmodifierは FillModifierとDrawBackgroundModifierだけで構成されており、!is ComposedModifier && it !is FocusEventModifier && it !is FocusRequesterModifierのものしかないことになります。なので、最初の
if (modifier.all {
// onFocusEvent is implemented now with ModifierLocals and SideEffects, but
// FocusEventModifier needs to have composition to do the same. The following
// check for FocusEventModifier is only needed until the modifier is removed.
// The same is true for FocusRequesterModifier and focusTarget()
it !is ComposedModifier && it !is FocusEventModifier && it !is FocusRequesterModifier
}
) {
return modifier
}
の部分で早期returnして、結局元のものと同じものが返ってくるだけになります。
と、話が簡単になるのでここはそういうシチュエーションを考えることにします。
従って、materializerOfメソッドのmodifierがそのままupdateメソッドに渡されるラムダの中のsetメソッド(これはより細かくいうとUpdater<ComposeUiNode>#setメソッドです。)の引数に使われることがわかったわけですが、次にupdate(SkippableUpdater<ComposeUiNode>#update)メソッドの実装を確認します。
2行目のUpdater<T>(composer).block()で、updateに渡したラムダが実行されています。SkippableUpdaterを作成したときのComposerを用いてUpdaterを作成し、それを使ってupdateメソッドにわたってきたUpdater<T>.()->Unitメソッドを呼び出しています、
また、updateに渡したラムダ(Updater<T>.()->Unitメソッドを)の中で呼び出しているUpdater<ComposeUiNode>#setメソッドの定義は以下です。

ここに出てくる、rememberedValue() != valueは、初回呼び出しではtrueになります。(はず)(どんどん横に外れてしまう&しっかり追えていないので飛ばしますが、rememberedValue()はComposerが持っているslotTableからnextの値を取ってくるもので、slotTableとは画面描写するための、色々な情報<どのようなコンポーネントがどう重なっているかなど>が入っているものです。まだ最初に描写する前は、ここはtrueになるはず。)
で、if文の中ですが、1行目のupdateRememberedValue(value)はslotTableの更新を行っていると思われるところで、これもちょっと脇道にそれるので省きます。そんなに複雑に絡み合っている場所ではなく、素直に定義ジャンプや実装先ジャンプをしていけば見れるので、気になる方は見てみてください。
大事なのは2行目のcomposer.apply(value, block)で、つまりこれは【composer=Updater作成時に渡したcomposer=SkippableUpdaterを作成したときのcomposer】のapplyメソッドを呼んでいます。引数は、前述のmaterializerOfメソッドの実装を見返すと、第一引数がmaterialized=Spacerに渡したmodifierそのもの、第二がComposeUiNode.SetModifierです。
さて、ここで少し話は戻ってしまいますが、「SkippableUpdaterを作成したときのcomposer」とはなんでしょうか。
そもそも話の始めに立ち返ると、このSkippableUpdaterとは、(既に出てきた)Layoutの中でReusableComposeNodeの引数skippableUpdateが実行されるときに使われるものであるはずです。
ということで、ReusableComposeNodeの実装を見てみます。

SkippableUpdater<T>(currentComposer).skippableUpdate()となっています。ということで、 「SkippableUpdaterを作成したときのcomposer」はこれもまたcurrentComposer でした。
さて、次に、composer.applyメソッドの実装を見てみます。
渡されてきた引数を用いて、Changeという関数型の変数operationを作成しています。
※ちなみにChangeの定義は以下です。

(applier: Applier<*>, slots: SlotWriter, rememberManager: RememberManager ) -> Unit なメソッドをtypealiasしてChangeと呼んでいるだけのようです。
で、ここで作ったChangeという関数型の変数operationですが、insertingの値に応じてrecordFixupまたはrecordApplierOperationの引数として渡されます。
注目すべきは3行目で、changeがrecordメソッドに渡されています。
recordはどういうメソッドかというと、以下のようなものです。
changesというMutableListにaddします。
そしてMutableList changesはCompositionImplクラスのフィールド変数private val composer: ComposerImplを作成する時に、これまた同じくCompositionImplのフィールド変数changesをセットする形で与えられます。
< `CompositionImpl` クラスのフィールド変数 `private val composer: ComposerImpl` >

<`private val composer: ComposerImpl`に渡している`changes`の定義>

で、CompositionImplのフィールド変数changes : ComposerImplは何に使われているかというと、CompositionImplのメソッドapplyChangesで、applyChangesInLockedの引数に使われます。

CompositionImplのapplyChangesInLockedメソッドの定義は以下です。
注目するべきは、赤枠で囲った部分です。また、赤枠の中で使われているapplierはCompositionImplを作成する時に与えるフィールド不変変数です。
changesに格納されているchange一つ一つに、applier渡しています。
つまり、composer.applyで作っていた changeがここのchangesに入っているわけで、そのchangeの定義は
val operation: Change = { applier, _, _ ->
@Suppress("UNCHECKED_CAST")
(applier.current as T).block(value)
}
で、このblockとvalueはComposeUiNode.SetModifierとmodifierです。
そして、今まで詳しく見ていませんでしたが、ComposeUiNode.SetModifierは、{ this.modifier = it }というComposeUiNode.(Modifier) -> Unit型の関数変数です。
なので、今までの話を総合すると、どこかで作られているCompositionImplのapplierのフィールド変数currentのmodifierに、CanvasのonDrawとModifierから作られたDrawBackgroundModifeirが、CompositionImplのapplyChangesが呼ばれた時にsetされる
ということがわかります。
なので、どこでCompositionImplが作られていて、その時のapplierが何で、いつapplyChangesが呼ばれるか注目していきたいね、という結論にここではなります。
※ちなみに、composer.applyメソッドの実装を見るところで、inserting==falseの時 を見ることで↑の結論を得たわけですが、trueの時はどうなるか見ていくと、
↑のrecordFixup(operation)が実行されます。
recordFixupの定義を確認すると、
何やらinsertUpFixupsというものにchangeをaddしています。
このinsertUpFixupsは、ComposerImplのフィールド変数のMutableList<Change>です。
そしてinsertUpFixupsは、ComposerImplのメソッドrecordInsertの中で、recordSlotEditingOperationメソッドが呼ばれるのですがそのメソッドの関数変数として渡しているラムダ敷の中で要素を一個ずつ実行します。
そしてrecordSlotEditingOperationの定義は以下で、引数数として渡されたラムダ式はinserting==trueの時同様にComposerImplのrecordメソッドの引数になります。

= つまりここからはinserting==trueの時と同じです。なので結論も同じです。
=どこかで作られているCompositionImplのapplierのフィールド変数currentのmodifierに、CanvasのonDrawとModifierから作られたDrawBackgroundModifeirが、CompositionImplのapplyChangesが呼ばれた時にsetされる
ということがわかります。
なので、どこでCompositionImplが作られていて、その時のapplierが何で、いつapplyChangesが呼ばれるか注目していきたいね、という結論にここではなります。
※ここで、recordInsertってどこで呼ばれるの?(本当に確実にどこかで呼ばれているの?使われているメソッドなの?)と思われるかもしれないので触れておくと、recordInsertはComposerImplのprivate fun end(isNode: Boolean)メソッドの中で使われています。
このendメソッドは、どうやら1つのComposeの塊を描き終わって次の単位に行く時に呼ばれるようです。なので、確実に呼ばれていると言っていいと思います。
③ComponentActivity.setContentを呼んだときの処理の流れを見ていく。それにより②で提示した疑問に答えを出す。
さて、ここで大きく見る場所を変えて、Composeを使用する時にComponentActivity(大体はAppCompatActivityとか)のonCreateで
ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
)
を呼ぶと思いますが、これが呼ばれた時に内部で何が起きるかを見ていきます。
まず、実装はこんな感じです。
何にせよ、ComposeView#setContentが呼ばれるようです。
(ちなみに、ComposeView#setContentの引数になっているcontentは、ComponentActivity#setContentを呼んだ時に渡しているラムダ部分全体です。)
※ここで確認しておくとComposeViewはAndroid ViewのView Groupを親に持つクラスです。
さて、ComposeView#setContentの内容を見ていきます。
渡されたcontentを、自分のフィールド変数contentのvalueにsetしています。その後createCompositionメソッドを呼んでいます。(正確には、isAttachedToWindows==trueなら、ですがこれはView#isAttachedToWindowメソッドです。画面にattachされていればtrueのはずです。)
さて、createComposition メソッドですが、ComposeViewの親のAbstractComposeViewのメソッドで処理の内容としてはensureCompositionCreateメソッドを呼んでいます。
ensureCompositionCreateメソッドはComposeViewに実装されており、AbstractComposeView#setContentメソッドをresolveParentCompositionContext()とComposeView#Content()を実行しているラムダ関数を引数に呼び出しています。
ComposeView#Content()は、content.valueを実行しています。これは先ほどsetしていたもので、中身はComponentActivity#setContentを呼んだ時に渡しているラムダ部分全体になっているはずですね。
ちなみに、resolveParentCompositionContext()は、同じくComposeViewのメソッドで、何かしらのCompositionContextになっているようです。
さて、AbstractComposeView#setContentメソッドはどんな内容になっているかというと、AndroidComposeViewというものを、ComposeViewの既存のchildから取ってくる or 新規作成して、それと、content(={Content()}ラムダ関数)を引数にdoSetContentメソッドを呼びます。ちなみにAndroidCompoiseViewはこれまたAndroidView時代のViewGroupを親に持つクラスです。
doSetContentメソッドでは、Composition(UiApplier(owner.root), parent)を作成して、これを用いてWrappedCompositionを作成して、WrappedComposition#setContentメソッドを呼びます。

WrappedComposition#setContentは何をしているかというとWrappedCompositionを作成したときに渡したAndroidComposeViewのsetOnViewTreeOwnersAvailableメソッドを呼びます。
setOnViewTreeOwnersAvailableメソッドでは、渡されたラムダがほぼそのまま呼び出されるので、setOnViewTreeOwnersAvailableメソッドに渡されているラムダ式の中身が大事です。
みてみると、LifecycleがCREATED以上の時に、WrappedCompositionを作成する時に渡したCompositionのsetContentメソッドを呼んで、その中で(CompositionLocalProviderとProvideAndroidCompositionLocalsを介して)contentを呼び出していることがわかります。
※詳しくはここでは割愛しますがCompositionLocalProviderは、ProvidedValue(Compose可能なメソッドの中でLocal~で取得できるContextとかLifeCycleOwnerとかの値)を上書きしたスコープで最後の引数のラムダメソッドを実行するメソッドです。ProvideAndroidCompositionLocalsはそれの特殊なバージョンで、中で最終的にCompositionLocalProviderを呼んでいます。

「WrappedCompositionを作成する時に渡したComposition」=Composition(UiApplier(owner.root), parent)のsetContentの実装をみてみると、prent.composeInitialの引数としてsetContentに渡したラムダを渡していることがわかります。
そしてprent.composeInitial(this, composable)ですが、まず、「Composition(UiApplier(owner.root), parent)」でのparentはAbstractComposeView#ensureCompositionCreatedで呼んでいたresolveParentCompositionContext()の戻りです。どういうものになっているかちょっとわからないですね。
ただ、ここのparentがどういう形になっているかがわからなくてもたぶん大丈夫です。というのは、composeInitial(composition: ControlledComposition,content: @Composable () -> Unit)を実装しているクラスは2つしかなく、その中で循環呼び出しになっていないのがRecomposerだけだからです。という状況証拠になってしまうのですが、どうやら最終的にRecomposer#composeInitial(composition: ControlledComposition,content: @Composable () -> Unit)が呼び出されるようです。
で、Recomposer#composeInitialの実装は以下です。
そして注目してほしいのは、赤枠の部分です。
composition.applyChanges()を呼んでいます。②のパートの結論では、
「どこかで作られているCompositionImplのapplierのフィールド変数currentのmodifierに、CanvasのonDrawとModifierから作られたDrawBackgroundModifeirが、CompositionImplのapplyChangesが呼ばれた時にsetされる」
「なので、どこでCompositionImplが作られていて、その時のapplierが何で、いつapplyChangesが呼ばれるか注目していきたいね、という結論にここではなります。」
という話がありましたがここがつながります。ここが探していた部分です。
(※「composeInitial(composition: ControlledComposition,content: @Composable () -> Unit)を実装しているクラスは2つ」しかないと書きましたが、もう1つはCompositionContextImplで、中身は渡された引数をそのまま使ってparentのcomposeInitial(composition: ControlledComposition,content: @Composable () -> Unit)をよんでいるだけです。
つまり、Composition(UiApplier(owner.root), parent)の持つメソッドの中で呼ばれているparent.composeInitial(this, composable)のthisやcomposableが、どこかでRecomposerにつながるまで引数としてそのまま渡され続けそうです。
)
----さてということで、②の結論で知りたい、といっていた点の答えは
- どこかで作られている
CompositionImpl:Composition(UiApplier(owner.root), parent)の持つメソッドの中で呼ばれているparent.composeInitial(this, composable)のthisですからComposition(UiApplier(owner.root), parent) - その時の
applierが何 :UiApplier(owner.root) - (いつ
applyChangesが呼ばれるか :Recomposer#composeInitial)
という風にまとめられます。
④CanvasのonDraw部分が実行されるときのDrawScopeが何かを得る
さて、②と③から、UiApplier(owner.root)のフィールド変数currentのmodifierに、CanvasのonDrawとModifierから作られたDrawBackgroundModifeirが、setされそうなことがわかりました。ということで今度は、UiApplier(owner.root)が何かということを見ていきます。
UiApplier()の定義は以下です。
フィールド変数currentは、UiApplierの親のAbstractApplier<LayoutNode>のフィールドで、コンストラクタで渡されたUiApplierのrootがそのままcurrentになります。

ということで、UiApplier(owner.root)のフィールド変数current = owner.rootということがわかりました。
次にowner.rootが何か、ということを見てみます。UiApplier(owner.root)を作成している部分は↓のような感じでした。
ownerはdoSetContentメソッドの引数として渡されてきています。doSetContentの呼び出し側は、
のような感じなので、ownerはAndroidComposeViewです。
ではAndroidComposeViewのrootとは、ということを確認すると、AndroidComposeViewクラス全体は大きすぎるのでroot部分だけを表示すると以下のようです。

さて、このrootのmodifierに、CanvasのonDrawとModifierから作られたDrawBackgroundModifeirが、setされることになります。
ところで、rootはLayoutNodeのインスタンスですが、LayoutNodeのmodifierのsetメソッドはただsetするだけではなく、以下のような処理をするものになっています。
全てを載せるには長すぎるので省略しますが、LayoutNodeのmodifierのsetメソッドで注目すべき場所は以下の部分です。
1個目の赤枠部で、modifierからouterWrapperを作成しています。ちなみにこの時点でmodifierは既にsetされた新しい値になっています。
outerWrapperはLayoutNodeWrapperという型です。LayoutNodeWrapperは、 LayoutNodeEntityのArrayをentitiesという名前で変数としてもつEntityListというクラスのインスタンスを、これまたentitiesという名前で持っています。そしてLayoutNodeEntityはLayoutNodeWrapperとModifierを一個ずつ持っています。
outerWrapperは、
val wrapper = if (mod is LayoutModifier) {
// Re-use the layoutNodeWrapper if possible.
(reuseLayoutNodeWrapper(toWrap, mod)
?: ModifiedLayoutNode(toWrap, mod)).apply { onInitialize() }
} else {
toWrap
}
で作成したLayoutNodeWrapperと、setされたmodifierを持つLayoutNodeEntityがentitiesに含まれているLayoutNodeWrapperです。

そして2個目の赤枠部でそれをouterMeasurablePlaceable.outerWrapperに代入しています。
LayoutNodeのouterMeasurablePlaceable.outerWrapperは、outerLayoutNodeWrapperをgetしたときの値として使われます。
つまり、AndroidComposeViewは、root.outerLayoutNodeWrapperを呼んだとき、CanvasのonDrawとModifierから作られたDrawBackgroundModifeirを持つLayoutNodeEntityがentitiesに含まれているLayoutNodeWrapperになっています。(考えているCompose可能なメソッドから作られた画面UIにCanvasがある場合)
さて、ここでAndroidComposeViewのdispatchDraw(canvas: android.graphics.Canvas)メソッドを見てみます。
このメソッドは、AndroidViewのViewGroupのdispatchDraw(canvas: android.graphics.Canvas)メソッドのoverrideです。
注目したいのは赤枠部で、
canvasHolder.drawInto(canvas) { root.draw(this) }
となっています。まず、canvasHolder.drawIntoの定義ですが以下です。

引数blockがandroidCanvasに対して実行されています。そしてandroidCanvasのinternalCanvasは、AndroidViewとしてのAndroidComposeViewのdispatchDrawに渡ってきているandroid.graphics.Canvas(つまりAndroidView時代のCanvas)になっています。
いま、引数blockは{ root.draw(this) }となっているはずです。
次に、root.draw(this)部分の処理を確認すると、まずroot.drawつまりLayoutNode#drawの定義は以下です。
outerLayoutNodeWrapperが呼ばれ、それに対してdraw(canvas: Canvas)メソッドが呼ばれています。ちなみに、引数になっているcanvas : Canvasはroot.draw(this)のthis、つまり「internalCanvasが、AndroidComposeViewのdispatchDrawに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvasです。
LayoutNodeWrapper#drawの定義は以下です。

簡単のためにlayerがnullの時(最初はnullらしい)の時だけ考えると、メインの処理はdrawContainedDrawModifiers(canvas)のようです。なのでdrawContainedDrawModifiersの定義を確認すると、
head==nullの時の処理performDraw(canvas)は、最終的に再度LayoutNode#draw(canvas: Canvas)メソッドに戻ってきてしまいます。
なので、head!=nullの時の方だけ考えると、head.draw(canvas)で呼び出されるメソッドは以下です。
着目するのは赤枠の部分で、layoutNode.mDrawScopeというLayoutNodeDrawScopeを取ってきて、drawScope.drawメソッドを呼んでいます。
そして、drawScope.drawメソッドの最後の引数となっているDrawScope.() -> Unitラムダの中で、modifierに対してContentDrawScope.draw()が呼ばれています。つまりCanvasについて考えている場合、modifierはCanvasのonDrawとModifierから作られたDrawBackgroundModifeirで、そのDrawBackgroundModifeirの持つContentDrawScope.drawメソッドの呼び出しでCanvasのonDrawで描いている部分も描かれるはずです。(①より。)
つまり、
with(modifier) {
draw()
}
が呼ばれている部分でのDrawScopeがCanvasのonDraw部分が実行されるときのDrawScopeなはずです。
ということで、LayoutNodeDrawScope#drawの処理内容を確認すると以下です。
(with(drawScope)で囲われているので、直接的なことを言えば「CanvasのonDraw部分が実行されるときのDrawScope」は絶対drawScopeな訳ですが、これがこの時にどういう状態のものなのかということが大事になってくるのであえてdrawScope.drawの処理内容から順に見ていきます。)

drawの第一引数のCanvasが、「internalCanvasが、AndroidComposeViewに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvas、第五引数のblockが処理内容が
with(drawScope) {
with(modifier) {
draw()
}
}
のDrawScope.() -> Unit型のラムダ関数です。
さて、赤枠の中の処理を確認します。まず、canvasDrawScopeはCanvasDrawScope型で、このLayoutNodeDrawScopeを作成するときに初期化されるフィールド変数です。また、LayoutNodeDrawScopeは親クラスにDrawScopeを持つのですが、これがby canvasDrawScopeでcanvasDrawScopeに委譲されているので親になっているとも言えます。
CanvasDrawScope#drawの定義は以下です。
1つ目の赤枠でdrawParamsというものが出てきますが、これはCanvasDrawScopeのもつ、DrawParams型の再代入不可のフィールド変数です。DrawParamsはdata classで、densityや canvasなど描写環境に関する設定を持たせておくもののようです。


1つ目の赤枠で、そのdrawParamsのcanvasを引数で渡ってきたcanvas、つまり「internalCanvasが、AndroidComposeViewのdispatchDrawに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvasになっています。
その状態で、2つ目の赤枠でthis.block()、つまりこのCanvasDrawScopeに対して
with(drawScope) {
with(modifier) {
draw()
}
}
を実行しています。
さて、CanvasのonDraw部分が実行されるときの直接のDrawScopeに関しては、
with(drawScope) {
with(modifier) {
draw()
}
}
の中の、
with(modifier) {
draw()
}
が実行されているDrawScopeのはずです。ここは、with(drawScope)で囲われているので、つまりdrawScopeなはずです。
そしてこの時、このLayoutNodeDrawScope型のdrawScopeの、フィールド変数であり親クラスの実体でもあるcanvasDrawScopeのdrawParams.canvasは「internalCanvasが、AndroidComposeViewのdispatchDrawに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvasです。
最後に、⑤章として実際にCanvasのonDrawの中でdrawImageを呼んだときの動きを見ていきます。その際に、上記の太字で書いた部分にまとめた内容が効いてきます。
⑤CanvasのonDrawの中でdrawImageを呼んだときは巡り巡ってAndroidViewのCanvasのdrawBitmapが呼ばれている
CanvasのonDrawの中でdrawImageを呼んで画像を描いたときの処理について追っていきます。
④章より、「CanvasのonDrawの中でdrawImageを呼ぶ」=「LayoutNodeDrawScopeのdrawImageを呼ぶ」ということです。しかしLayoutNodeDrawScope自身はdrawImageメソッドを実装していないので親であるcanvasDrawScope のdrawImageがその時に呼ばれるものになります。
それは以下です。
それぞれ、drawParams.canvasのdrawImageやdrawImageRectを呼んでいます。drawParams.canvasは、④よりAndroidCanvasのはずです。よって、AndroidCanvasのdrawImageやdrawImageRectの実装を確認します。
両方とも、internalCanvas.drawBitmapメソッドを呼んでいます。しつこいですが、この時internalCanvasはAndroidViewの1クラスとしてのAndroidComposeViewのdispatchDrawに渡ってきているandroid.graphics.Canvasになっているはずです。
=> android.graphics.CanvasのdrawBitmapに関する問題に帰着させられました。つまりここから先はAndroidView時代のViewを用いた場合と同じ処理が走るはずです。なので、ComposeでCanvasに画像を描写したときにBlendModeの挙動が想定外になる原因は、AndroidViewのCanvasの時と同じ、といってよさそうです。
BlendModeの予想外挙動は、全てAndroidViewのCanvasの動きに依存しているといってよさそうです。
厳密には....
-
AndroidComposeView(=ある特定のViewGroup)とViewを継承した自作カスタムクラスに渡ってくるCanvasは本当に同じものなのか? - そもそもAndroidViewの
Viewでやる時はonDraw、今回AndroidComposeViewで考えている時はdispatchDrawに渡ってきているCanvasを使用しているがそこに違いはないのか? - 今回のコードリーティングの過程で何点か状況証拠や推測で誤魔化しているところがあるけどそこは大丈夫?
などツッコミどころはいくつかあるかと思うのですが、とりあえずこれでいいということにします、、、、これ以上厳密に見ていくのはちょっと実力不足時間不足でした。
何か指摘・疑問点などあればコメントいただければと思います。
せっかくなので、今後も使えそうなTips
- Composeの
CanvasのonDrawの中で呼んだ動きの挙動について疑問点が生まれたら...-
LayoutNodeDrawScopeのそのメソッドの実装を確認する(それが呼ばれている) -
LayoutNodeDrawScopeにその実装がなかったらCanvasDrawScopeのそれを確認する(それが呼ばれている) - その時、
CanvasDrawScopeのcanvasはAndroidCanvasになっている。 - さらにその
canvasのinternalCanvasはViewGroupの1つであるであるAndroidComposeViewのdispatchDrawに渡ってきているandroid.graphics.Canvasになっている。
-
- Compose可能なメソッドで作成したUIも、(`ComponentActivity.setContent`を使用する場合、)内部的にはひとつ
ComposeViewというViewGroupを置いて、さらにその中にAndroidComoseViewというものを置いて、そこにComposeで作ったものをこちゃこちゃと置いている=深い部分ではAndroidView時代と共通部がある・AndroidView時代の知識が役に立つこともそれなりにありそう。
参考にしたもの
@takahiromさんの以下の一連のコードリーディングはとても助けになりました。ありがとうございます。
https://qiita.com/takahirom/items/d2a89560f8ff2065a7c0
https://qiita.com/takahirom/items/0e72bee081de8cf4f05f
https://qiita.com/takahirom/items/11e3ed72eb2f83440b12
https://qiita.com/takahirom/items/0e0a3559d95b49399c3b

















































