LoginSignup
4
2

More than 1 year has passed since last update.

JetpackComposeでCanvasにImageを描写したときにBlendModeの挙動が想定外になる原因は、AndroidViewの時と同じ

Last updated at Posted at 2023-01-09

ということを納得するためにJetpackComposeCanvas周りのコードリーディングをしました。
その備忘録です。

この投稿を使用している、DroidKaigiの資料のリンク

背景 (長いので読まなくていいです。)

2022/10にあったDroidKaigiでJetpackComposeCanvasをテーマに登壇しました。
その際に、「画像を描写した時のBlendModeの挙動が定義と異なっているように見える部分がある」、「何もかもが不明」、「何かご存知の方がいらっしゃったら教えて欲しい」としてあった部分がありました。
そこについて、解決法を教えてくださった方があり、なので登壇資料をアップデートしようかなと色々試していたら、そもそも件のBlendModeの動きはAndroidViewCanvasでも同じように振る舞うことが判明しました。そして何となく原因も推定できました。

そこからさらにJetpackComposeの方のComposeライブラリのコードを読んでいるとなんとなく「この挙動は根本的にはAndroidViewCanvasの挙動がこうだからこうなっているのだな。全てはAndroidViewCanvasの動きに依存しているのだな」ということが察せられてきました。

が、とはいえ簡単には資料で言い切れるほどの自信が持てませんでした。
最終的には自信を持てるところまでコードリーディングをしたのですが、結果、すごく時間がかかったし、(資料の要旨がぼやけるので)スライドに載せるべき分量ではなくなるし、だったので自分向け備忘録および資料をみたときになんでそう言い切っているのか気になった方がいた場合のためにQiitaにまとめて公開している次第です。

環境

Compose v1.2.0
Kotlin v1.7.0

※DroidKaigi資料に合わせているのでちょっと古めです。

コードリーディング内容

非常に長いので、以下のように5段階にわけてみました。

⓪AndroidView時代のViewで画像を描写するときの復習
ComposeCanvasは、onDrawで渡したものもModifierになる
CanvasonDrawModifierから作られたDrawBackgroundModifeirがどこでどういう風に使われていくか見ていく
ComponentActivity.setContentを呼んだときの処理の流れを見ていく。それにより②で提示した疑問に答えを出す。
CanvasonDraw部分が実行されるときのDrawScopeが何かを得る
CanvasonDrawの中でdrawImageを呼んだときは巡り巡ってAndroidViewCanvasdrawBitmapが呼ばれている

まず、確認として、ComposeCanvasで画像を描写するときの方法を簡単に載せます。(詳しくは、冒頭に紹介したDroidKaigiのスライドの前半部分に書かれています。)
以下はスライドにもスクリーンショットを載せていた、ComposeCanvasonDrawとして渡しているDrawScope.()->Unitラムダの一部です。画像を描写するためにDrawScope#drawImageメソッドを呼んでいます。(BlendModeも指定しています。)
これはつまり、ComposeCanvasで画像を描写するときの実際の処理は、この onDrawラムダを呼び出してくるDrawScopeを実装したインスタンスがどんなクラスで、どんなdrawImageの実装を持つかに依存するということです。(DrawScopeはインターフェイスです。)
スクリーンショット 2022-11-27 14.18.30.png

⓪AndroidView時代のViewで画像を描写するときの復習

  • View#onDrawの引数で渡ってくるandroid.graphics.CanvasdrawBitmapメソッドを呼びます。
  • この時に関しても、(ComposeのCanvasの時と同じく)背景透過画像の上からDstOverで他の画像を描き込んだ時は背景が上手く透過しない = AndroidView時代から、背景透過画像の上からDstOverで他の画像を描き込んだ時はBlendModeが定義通りには動いていないらしい
  • どうやらこれはハードウェア アクセラレーションのせいらしい(推測)

※詳しくはDroidKaigi資料の110ページ目〜に譲ります。

ComposeCanvasは、onDrawで渡したものもModifierになる

以下がComposeCanvasの定義です。onDrawに渡したDrawScope.()->Unitラムダがmodifierのメソッドに渡され、(それと合成されて。詳しくは後述。)SpacerメソッドのModifierとして渡されていることがわかります。

スクリーンショット 2022-11-27 14.26.04.png

スクリーンショット 2022-11-27 14.30.25.png

ここでmodifier.drawBehind(onDraw)の処理の中身を見てみます。こんな感じです。

スクリーンショット 2022-11-27 14.33.01.png

onDrawDrawBackgroundModifierとなり、Canvasに渡されたmodifierthenメソッドの引数として利用されています。Modifier#thenメソッドの詳細な内部実装は当然Modifierによって違うと思いますが、「自分と他のModifierを連結する」メソッドになります。つまり、onDrawから作ったDrawBackgroundModifierは、Canvasに元々渡されたModifierに連結されてひとつのModifierになるわけです。

ここで、DrawBackgroundModifierの定義を見てみます。
スクリーンショット 2022-11-27 14.39.24.png

Canvasに渡されたonDrawは、、DrawBackgroundModifierContentDrawScope.drawメソッドの中で呼び出されるようです。
つまり、「CanvasonDrawラムダを呼び出してくるDrawScopeを実装したインスタンスがどんなクラスで、どんなdrawImageの実装を持つか」を知りたかったわけですが、それは 「設定されているModifierContentDrawScope.drawメソッドを呼び出すのがどんなContentDrawScopeインスタンスなのか」を探っていくことに帰着する ことがわかりました。(ちなみにContentDrawScopはDrawScope`を継承したインターフェースです。)

スクリーンショット 2022-11-27 14.53.35.png

CanvasonDrawModifierから作られたDrawBackgroundModifeirがどこでどういう風に使われていくか見ていく。

さて、ここでちょっと視点を一階層上に戻して、DrawScope.()->UnitラムダがModifierとなった後にSpacerに渡され、以降引き渡されていくかを見ていきたいと思います。

さっきも出てきましたが、Spacerの定義は以下です。そしてその中の唯一の処理になっているLayoutの定義は以下です。

スクリーンショット 2022-11-27 14.30.25.png

スクリーンショット 2022-11-27 15.06.39.png

(Canvasに渡したmodifieronDrawから作成したModifierが連結された)modifierは、materializerOfメソッドに渡され、その戻りがReusableComposeNode<ComposeUiNode, Applier<Any>>メソッドのskippableUpdateとして使われています。

materializerOfメソッドの実装は以下です。

スクリーンショット 2022-11-27 15.10.26.png

最初にmodifiercurrentComposer.materializeメソッドに渡され、戻りがupdateメソッドに渡されるラムダの中のsetメソッド(これはより細かくいうとUpdater<ComposeUiNode>#setメソッドです。)の引数に使われています。

このcurrentComposerの定義は以下です。
スクリーンショット 2022-11-27 15.10.59.png

実は、具体的にcurrentComposerを誰がセットしているのか、結局最後までわかりませんでした。が、Composerインターフェースはsealedインターフェイスであり、実装されているクラスはComposerImplしかないので、currentComposer#materializeの実装は特定できるのでとりあえず先に進めます。

currentComposer#materializeの実装は以下です。

スクリーンショット 2022-11-27 15.16.21.png
長いですが、Canvasに渡したmodifierがたとえば極めて単純なModifier.fillMaxSize()だった場合、ここに渡ってくるmodifier FillModifierDrawBackgroundModifierだけで構成されており、!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)メソッドの実装を確認します。

スクリーンショット 2022-11-27 15.39.28.png

2行目のUpdater<T>(composer).block()で、updateに渡したラムダが実行されています。SkippableUpdaterを作成したときのComposerを用いてUpdaterを作成し、それを使ってupdateメソッドにわたってきたUpdater<T>.()->Unitメソッドを呼び出しています、
また、updateに渡したラムダ(Updater<T>.()->Unitメソッドを)の中で呼び出しているUpdater<ComposeUiNode>#setメソッドの定義は以下です。
スクリーンショット 2022-11-27 15.43.28.png

ここに出てくる、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が実行されるときに使われるものであるはずです。

スクリーンショット 2022-11-27 15.06.39.png

ということで、ReusableComposeNodeの実装を見てみます。
スクリーンショット 2023-01-08 15.35.30.png

SkippableUpdater<T>(currentComposer).skippableUpdate()となっています。ということで、 SkippableUpdaterを作成したときのcomposer」はこれもまたcurrentComposer でした。


さて、次に、composer.applyメソッドの実装を見てみます。

スクリーンショット 2022-11-27 18.26.07.png

渡されてきた引数を用いて、Changeという関数型の変数operationを作成しています。
※ちなみにChangeの定義は以下です。
スクリーンショット 2022-11-27 18.29.57.png

(applier: Applier<*>, slots: SlotWriter, rememberManager: RememberManager ) -> Unit なメソッドをtypealiasしてChangeと呼んでいるだけのようです。

で、ここで作ったChangeという関数型の変数operationですが、insertingの値に応じてrecordFixupまたはrecordApplierOperationの引数として渡されます。

  • inserting==falseの時
    recordApplierOperationが呼ばれます。定義は以下です。
    スクリーンショット 2022-11-27 21.30.16.png

注目すべきは3行目で、changerecordメソッドに渡されています。
recordはどういうメソッドかというと、以下のようなものです。

スクリーンショット 2022-11-27 21.32.24.png

changesというMutableListaddします。

スクリーンショット 2022-11-27 21.34.15.png

そしてMutableList changesCompositionImplクラスのフィールド変数private val composer: ComposerImplを作成する時に、これまた同じくCompositionImplのフィールド変数changesをセットする形で与えられます。

< `CompositionImpl` クラスのフィールド変数 `private val composer: ComposerImpl` >
スクリーンショット 2022-12-06 21.12.24.png

<`private val composer: ComposerImpl`に渡している`changes`の定義>
スクリーンショット 2022-12-06 21.13.31.png

で、CompositionImplのフィールド変数changes : ComposerImplは何に使われているかというと、CompositionImplのメソッドapplyChangesで、applyChangesInLockedの引数に使われます。
スクリーンショット 2022-12-06 21.16.58.png

CompositionImplapplyChangesInLockedメソッドの定義は以下です。
注目するべきは、赤枠で囲った部分です。また、赤枠の中で使われているapplierCompositionImplを作成する時に与えるフィールド不変変数です。

スクリーンショット 2022-12-06 21.19.42.png

スクリーンショット 2022-12-06 21.23.45.png

changesに格納されているchange一つ一つに、applier渡しています。

つまり、composer.applyで作っていた changeがここのchangesに入っているわけで、そのchangeの定義は

        val operation: Change = { applier, _, _ ->
            @Suppress("UNCHECKED_CAST")
            (applier.current as T).block(value)
        }

で、このblockvalueComposeUiNode.SetModifiermodifierです。
そして、今まで詳しく見ていませんでしたが、ComposeUiNode.SetModifierは、{ this.modifier = it }というComposeUiNode.(Modifier) -> Unit型の関数変数です。

スクリーンショット 2022-12-06 21.35.20.png

なので、今までの話を総合すると、どこかで作られているCompositionImplapplierのフィールド変数currentmodifierに、CanvasonDrawModifierから作られたDrawBackgroundModifeirが、CompositionImplapplyChangesが呼ばれた時にsetされる
ということがわかります。
なので、どこでCompositionImplが作られていて、その時のapplierが何で、いつapplyChangesが呼ばれるか注目していきたいね、という結論にここではなります。

※ちなみに、composer.applyメソッドの実装を見るところで、inserting==falseの時 を見ることで↑の結論を得たわけですが、trueの時はどうなるか見ていくと、

<再掲>
スクリーンショット 2022-11-27 18.26.07.png

↑のrecordFixup(operation)が実行されます。
recordFixupの定義を確認すると、

スクリーンショット 2022-12-12 21.17.18.png

何やらinsertUpFixupsというものにchangeaddしています。
このinsertUpFixupsは、ComposerImplのフィールド変数のMutableList<Change>です。

スクリーンショット 2022-12-12 21.25.25.png

そしてinsertUpFixupsは、ComposerImplのメソッドrecordInsertの中で、recordSlotEditingOperationメソッドが呼ばれるのですがそのメソッドの関数変数として渡しているラムダ敷の中で要素を一個ずつ実行します。

スクリーンショット 2022-12-12 21.25.41.png

そしてrecordSlotEditingOperationの定義は以下で、引数数として渡されたラムダ式はinserting==trueの時同様にComposerImplrecordメソッドの引数になります。
スクリーンショット 2022-12-12 21.25.52.png

= つまりここからはinserting==trueの時と同じです。なので結論も同じです。
=どこかで作られているCompositionImplapplierのフィールド変数currentmodifierに、CanvasonDrawModifierから作られたDrawBackgroundModifeirが、CompositionImplapplyChangesが呼ばれた時にsetされる
ということがわかります。
なので、どこでCompositionImplが作られていて、その時のapplierが何で、いつapplyChangesが呼ばれるか注目していきたいね、という結論にここではなります。

※ここで、recordInsertってどこで呼ばれるの?(本当に確実にどこかで呼ばれているの?使われているメソッドなの?)と思われるかもしれないので触れておくと、recordInsertComposerImplprivate fun end(isNode: Boolean)メソッドの中で使われています。
このendメソッドは、どうやら1つのComposeの塊を描き終わって次の単位に行く時に呼ばれるようです。なので、確実に呼ばれていると言っていいと思います。

ComponentActivity.setContentを呼んだときの処理の流れを見ていく。それにより②で提示した疑問に答えを出す。

さて、ここで大きく見る場所を変えて、Composeを使用する時にComponentActivity(大体はAppCompatActivityとか)のonCreate

ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) 

を呼ぶと思いますが、これが呼ばれた時に内部で何が起きるかを見ていきます。
まず、実装はこんな感じです。

スクリーンショット 2022-12-14 21.32.08.png

何にせよ、ComposeView#setContentが呼ばれるようです。

スクリーンショット 2022-12-14 21.32.08.png

(ちなみに、ComposeView#setContentの引数になっているcontentは、ComponentActivity#setContentを呼んだ時に渡しているラムダ部分全体です。)

※ここで確認しておくとComposeViewはAndroid ViewのView Groupを親に持つクラスです。

スクリーンショット 2022-12-27 16.53.39.png
スクリーンショット 2022-12-27 16.53.52.png

さて、ComposeView#setContentの内容を見ていきます。

スクリーンショット 2022-12-14 21.38.54.png

スクリーンショット 2022-12-14 21.39.57のコピー.png

渡されたcontentを、自分のフィールド変数contentvalueにsetしています。その後createCompositionメソッドを呼んでいます。(正確には、isAttachedToWindows==trueなら、ですがこれはView#isAttachedToWindowメソッドです。画面にattachされていればtrueのはずです。)

さて、createComposition メソッドですが、ComposeViewの親のAbstractComposeViewのメソッドで処理の内容としてはensureCompositionCreateメソッドを呼んでいます。

スクリーンショット 2022-12-27 17.10.47.png

ensureCompositionCreateメソッドはComposeViewに実装されており、AbstractComposeView#setContentメソッドをresolveParentCompositionContext()ComposeView#Content()を実行しているラムダ関数を引数に呼び出しています。
ComposeView#Content()は、content.valueを実行しています。これは先ほどsetしていたもので、中身はComponentActivity#setContentを呼んだ時に渡しているラムダ部分全体になっているはずですね。

スクリーンショット 2022-12-27 17.10.58.png

スクリーンショット 2022-12-27 17.13.01.png

ちなみに、resolveParentCompositionContext()は、同じくComposeViewのメソッドで、何かしらのCompositionContextになっているようです。

スクリーンショット 2022-12-27 17.16.30.png

さて、AbstractComposeView#setContentメソッドはどんな内容になっているかというと、AndroidComposeViewというものを、ComposeViewの既存のchildから取ってくる or 新規作成して、それと、content(={Content()}ラムダ関数)を引数にdoSetContentメソッドを呼びます。ちなみにAndroidCompoiseViewはこれまたAndroidView時代のViewGroupを親に持つクラスです。

スクリーンショット 2022-12-27 18.25.46.png

スクリーンショット 2022-12-27 18.30.17.png

doSetContentメソッドでは、Composition(UiApplier(owner.root), parent)を作成して、これを用いてWrappedCompositionを作成して、WrappedComposition#setContentメソッドを呼びます。
スクリーンショット 2022-12-27 20.37.49.png

WrappedComposition#setContentは何をしているかというとWrappedCompositionを作成したときに渡したAndroidComposeViewsetOnViewTreeOwnersAvailableメソッドを呼びます。
setOnViewTreeOwnersAvailableメソッドでは、渡されたラムダがほぼそのまま呼び出されるので、setOnViewTreeOwnersAvailableメソッドに渡されているラムダ式の中身が大事です。

スクリーンショット 2022-12-27 20.40.12.png
スクリーンショット 2022-12-27 20.40.56.png

みてみると、LifecycleCREATED以上の時に、WrappedCompositionを作成する時に渡したCompositionsetContentメソッドを呼んで、その中で(CompositionLocalProviderProvideAndroidCompositionLocalsを介して)contentを呼び出していることがわかります。
※詳しくはここでは割愛しますがCompositionLocalProviderは、ProvidedValue(Compose可能なメソッドの中でLocal~で取得できるContextとかLifeCycleOwnerとかの値)を上書きしたスコープで最後の引数のラムダメソッドを実行するメソッドです。ProvideAndroidCompositionLocalsはそれの特殊なバージョンで、中で最終的にCompositionLocalProviderを呼んでいます。
スクリーンショット 2022-12-27 21.01.47.png

WrappedCompositionを作成する時に渡したComposition」=Composition(UiApplier(owner.root), parent)setContentの実装をみてみると、prent.composeInitialの引数としてsetContentに渡したラムダを渡していることがわかります。

スクリーンショット 2022-12-27 20.59.55.png

そしてprent.composeInitial(this, composable)ですが、まず、「Composition(UiApplier(owner.root), parent)」でのparentAbstractComposeView#ensureCompositionCreatedで呼んでいたresolveParentCompositionContext()の戻りです。どういうものになっているかちょっとわからないですね。

ただ、ここのparentがどういう形になっているかがわからなくてもたぶん大丈夫です。というのは、composeInitial(composition: ControlledComposition,content: @Composable () -> Unit)を実装しているクラスは2つしかなく、その中で循環呼び出しになっていないのがRecomposerだけだからです。という状況証拠になってしまうのですが、どうやら最終的にRecomposer#composeInitial(composition: ControlledComposition,content: @Composable () -> Unit)が呼び出されるようです。
で、Recomposer#composeInitialの実装は以下です。

スクリーンショット 2023-01-08 17.05.37.png

そして注目してほしいのは、赤枠の部分です。
composition.applyChanges()を呼んでいます。②のパートの結論では、
どこかで作られているCompositionImplapplierのフィールド変数currentmodifierに、CanvasonDrawModifierから作られたDrawBackgroundModifeirが、CompositionImplapplyChangesが呼ばれた時にsetされる
「なので、どこでCompositionImplが作られていて、その時のapplierが何で、いつapplyChangesが呼ばれるか注目していきたいね、という結論にここではなります。」
という話がありましたがここがつながります。ここが探していた部分です。

(※「composeInitial(composition: ControlledComposition,content: @Composable () -> Unit)を実装しているクラスは2つ」しかないと書きましたが、もう1つはCompositionContextImplで、中身は渡された引数をそのまま使ってparentのcomposeInitial(composition: ControlledComposition,content: @Composable () -> Unit)をよんでいるだけです。

スクリーンショット 2023-01-08 17.53.46.png

つまり、Composition(UiApplier(owner.root), parent)の持つメソッドの中で呼ばれているparent.composeInitial(this, composable)thiscomposableが、どこかでRecomposerにつながるまで引数としてそのまま渡され続けそうです。
)

----さてということで、②の結論で知りたい、といっていた点の答えは

  • どこかで作られているCompositionImpl : Composition(UiApplier(owner.root), parent)の持つメソッドの中で呼ばれているparent.composeInitial(this, composable)thisですからComposition(UiApplier(owner.root), parent)
  • その時のapplierが何  : UiApplier(owner.root)
  • (いつapplyChangesが呼ばれるか : Recomposer#composeInitial)
    という風にまとめられます。

CanvasonDraw部分が実行されるときのDrawScopeが何かを得る

さて、②と③から、UiApplier(owner.root)のフィールド変数currentmodifierに、CanvasonDrawModifierから作られたDrawBackgroundModifeirが、setされそうなことがわかりました。ということで今度は、UiApplier(owner.root)が何かということを見ていきます。

UiApplier()の定義は以下です。

スクリーンショット 2023-01-08 18.15.29.png

フィールド変数currentは、UiApplierの親のAbstractApplier<LayoutNode>のフィールドで、コンストラクタで渡されたUiApplierrootがそのままcurrentになります。
スクリーンショット 2023-01-08 18.15.56.png

ということで、UiApplier(owner.root)のフィールド変数current = owner.rootということがわかりました。
次にowner.rootが何か、ということを見てみます。UiApplier(owner.root)を作成している部分は↓のような感じでした。

スクリーンショット 2022-12-27 20.37.49.png

ownerdoSetContentメソッドの引数として渡されてきています。doSetContentの呼び出し側は、

スクリーンショット 2023-01-08 18.22.05.png

のような感じなので、ownerAndroidComposeViewです。
ではAndroidComposeViewrootとは、ということを確認すると、AndroidComposeViewクラス全体は大きすぎるのでroot部分だけを表示すると以下のようです。
スクリーンショット 2023-01-08 18.31.29.png

さて、このrootmodifierに、CanvasonDrawModifierから作られたDrawBackgroundModifeirが、setされることになります。
ところで、rootLayoutNodeのインスタンスですが、LayoutNodemodifiersetメソッドはただsetするだけではなく、以下のような処理をするものになっています。

スクリーンショット 2023-01-08 20.41.19.png

全てを載せるには長すぎるので省略しますが、LayoutNodemodifiersetメソッドで注目すべき場所は以下の部分です。

スクリーンショット 2023-01-08 20.43.50.png

1個目の赤枠部で、modifierからouterWrapperを作成しています。ちなみにこの時点でmodifierは既にsetされた新しい値になっています。
outerWrapperLayoutNodeWrapperという型です。LayoutNodeWrapperは、 LayoutNodeEntityのArrayをentitiesという名前で変数としてもつEntityListというクラスのインスタンスを、これまたentitiesという名前で持っています。そしてLayoutNodeEntityLayoutNodeWrapperModifierを一個ずつ持っています。
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を持つLayoutNodeEntityentitiesに含まれているLayoutNodeWrapperです。
スクリーンショット 2023-01-08 21.11.05.png

そして2個目の赤枠部でそれをouterMeasurablePlaceable.outerWrapperに代入しています。
LayoutNodeouterMeasurablePlaceable.outerWrapperは、outerLayoutNodeWrapperをgetしたときの値として使われます。

スクリーンショット 2023-01-08 20.48.36.png

つまり、AndroidComposeViewは、root.outerLayoutNodeWrapperを呼んだとき、CanvasonDrawModifierから作られたDrawBackgroundModifeirを持つLayoutNodeEntityentitiesに含まれているLayoutNodeWrapperになっています。(考えているCompose可能なメソッドから作られた画面UIにCanvasがある場合)

さて、ここでAndroidComposeViewdispatchDraw(canvas: android.graphics.Canvas)メソッドを見てみます。
このメソッドは、AndroidViewのViewGroupdispatchDraw(canvas: android.graphics.Canvas)メソッドのoverrideです。

スクリーンショット 2023-01-08 20.54.10.png

注目したいのは赤枠部で、

canvasHolder.drawInto(canvas) { root.draw(this) }

となっています。まず、canvasHolder.drawIntoの定義ですが以下です。
スクリーンショット 2023-01-08 20.56.22.png

引数blockandroidCanvasに対して実行されています。そしてandroidCanvasinternalCanvasは、AndroidViewとしてのAndroidComposeViewdispatchDrawに渡ってきているandroid.graphics.Canvas(つまりAndroidView時代のCanvas)になっています。
いま、引数block{ root.draw(this) }となっているはずです。

次に、root.draw(this)部分の処理を確認すると、まずroot.drawつまりLayoutNode#drawの定義は以下です。

スクリーンショット 2023-01-08 21.21.57.png

outerLayoutNodeWrapperが呼ばれ、それに対してdraw(canvas: Canvas)メソッドが呼ばれています。ちなみに、引数になっているcanvas : Canvasroot.draw(this)this、つまり「internalCanvasが、AndroidComposeViewdispatchDrawに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvasです。

LayoutNodeWrapper#drawの定義は以下です。
スクリーンショット 2023-01-08 21.23.51.png

簡単のためにlayerがnullの時(最初はnullらしい)の時だけ考えると、メインの処理はdrawContainedDrawModifiers(canvas)のようです。なのでdrawContainedDrawModifiersの定義を確認すると、

スクリーンショット 2023-01-08 21.23.59.png
です。

head==nullの時の処理performDraw(canvas)は、最終的に再度LayoutNode#draw(canvas: Canvas)メソッドに戻ってきてしまいます。
なので、head!=nullの時の方だけ考えると、head.draw(canvas)で呼び出されるメソッドは以下です。

スクリーンショット 2023-01-08 21.31.55.png

着目するのは赤枠の部分で、layoutNode.mDrawScopeというLayoutNodeDrawScopeを取ってきて、drawScope.drawメソッドを呼んでいます。
そして、drawScope.drawメソッドの最後の引数となっているDrawScope.() -> Unitラムダの中で、modifierに対してContentDrawScope.draw()が呼ばれています。つまりCanvasについて考えている場合、modifierCanvasonDrawModifierから作られたDrawBackgroundModifeirで、そのDrawBackgroundModifeirの持つContentDrawScope.drawメソッドの呼び出しでCanvasonDrawで描いている部分も描かれるはずです。(①より。)

つまり、

                with(modifier) {
                    draw()
                }

が呼ばれている部分でのDrawScopeCanvasonDraw部分が実行されるときのDrawScopeなはずです。

ということで、LayoutNodeDrawScope#drawの処理内容を確認すると以下です。
(with(drawScope)で囲われているので、直接的なことを言えば「CanvasonDraw部分が実行されるときのDrawScope」は絶対drawScopeな訳ですが、これがこの時にどういう状態のものなのかということが大事になってくるのであえてdrawScope.drawの処理内容から順に見ていきます。)
スクリーンショット 2023-01-09 13.13.12.png

drawの第一引数のCanvasが、「internalCanvasが、AndroidComposeViewに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvas、第五引数のblockが処理内容が

 with(drawScope) {
                with(modifier) {
                    draw()
                }
            }

DrawScope.() -> Unit型のラムダ関数です。

さて、赤枠の中の処理を確認します。まず、canvasDrawScopeCanvasDrawScope型で、このLayoutNodeDrawScopeを作成するときに初期化されるフィールド変数です。また、LayoutNodeDrawScopeは親クラスにDrawScopeを持つのですが、これがby canvasDrawScopecanvasDrawScopeに委譲されているので親になっているとも言えます。

スクリーンショット 2023-01-09 13.21.51.png

CanvasDrawScope#drawの定義は以下です。

スクリーンショット 2023-01-09 13.26.58.png

1つ目の赤枠でdrawParamsというものが出てきますが、これはCanvasDrawScopeのもつ、DrawParams型の再代入不可のフィールド変数です。DrawParamsdata classで、densityや canvasなど描写環境に関する設定を持たせておくもののようです。
スクリーンショット 2023-01-09 13.32.39.png
スクリーンショット 2023-01-09 13.32.25.png

1つ目の赤枠で、そのdrawParamscanvasを引数で渡ってきたcanvas、つまり「internalCanvasが、AndroidComposeViewdispatchDrawに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvasになっています。
その状態で、2つ目の赤枠でthis.block()、つまりこのCanvasDrawScopeに対して

 with(drawScope) {
                with(modifier) {
                    draw()
                }
            }

を実行しています。

さて、CanvasonDraw部分が実行されるときの直接のDrawScopeに関しては、

 with(drawScope) {
                with(modifier) {
                    draw()
                }
            }

の中の、

                with(modifier) {
                    draw()
                }

が実行されているDrawScopeのはずです。ここは、with(drawScope)で囲われているので、つまりdrawScopeなはずです。
そしてこの時、このLayoutNodeDrawScope型のdrawScopeの、フィールド変数であり親クラスの実体でもあるcanvasDrawScopedrawParams.canvasは「internalCanvasが、AndroidComposeViewdispatchDrawに渡ってきているandroid.graphics.Canvasになっている」AndroidCanvasです。
最後に、⑤章として実際にCanvasonDrawの中でdrawImageを呼んだときの動きを見ていきます。その際に、上記の太字で書いた部分にまとめた内容が効いてきます。

CanvasonDrawの中でdrawImageを呼んだときは巡り巡ってAndroidViewCanvasdrawBitmapが呼ばれている

CanvasonDrawの中でdrawImageを呼んで画像を描いたときの処理について追っていきます。

④章より、「CanvasonDrawの中でdrawImageを呼ぶ」=「LayoutNodeDrawScopedrawImageを呼ぶ」ということです。しかしLayoutNodeDrawScope自身はdrawImageメソッドを実装していないので親であるcanvasDrawScope のdrawImageがその時に呼ばれるものになります。
それは以下です。

  • topLeftのみ設定できるdrawImage
    スクリーンショット 2023-01-09 15.09.53.png

  • 細かく設定できるdrawImage
    スクリーンショット 2023-01-09 15.07.49.png

それぞれ、drawParams.canvasdrawImagedrawImageRectを呼んでいます。drawParams.canvasは、④よりAndroidCanvasのはずです。よって、AndroidCanvasdrawImagedrawImageRectの実装を確認します。

  • drawImage
    スクリーンショット 2023-01-09 15.10.15.png

  • drawImageRect
    スクリーンショット 2023-01-09 15.10.47.png

両方とも、internalCanvas.drawBitmapメソッドを呼んでいます。しつこいですが、この時internalCanvasはAndroidViewの1クラスとしてのAndroidComposeViewdispatchDrawに渡ってきているandroid.graphics.Canvasになっているはずです。

=> android.graphics.CanvasdrawBitmapに関する問題に帰着させられました。つまりここから先はAndroidView時代のViewを用いた場合と同じ処理が走るはずです。なので、ComposeでCanvasに画像を描写したときにBlendModeの挙動が想定外になる原因は、AndroidViewのCanvasの時と同じ、といってよさそうです。
BlendModeの予想外挙動は、全てAndroidViewCanvasの動きに依存しているといってよさそうです。

厳密には....

  • AndroidComposeView(=ある特定のViewGroup)とViewを継承した自作カスタムクラスに渡ってくるCanvasは本当に同じものなのか?
  • そもそもAndroidViewのViewでやる時はonDraw、今回AndroidComposeViewで考えている時はdispatchDrawに渡ってきているCanvasを使用しているがそこに違いはないのか?
  • 今回のコードリーティングの過程で何点か状況証拠や推測で誤魔化しているところがあるけどそこは大丈夫?

などツッコミどころはいくつかあるかと思うのですが、とりあえずこれでいいということにします、、、、これ以上厳密に見ていくのはちょっと実力不足時間不足でした。
何か指摘・疑問点などあればコメントいただければと思います。

せっかくなので、今後も使えそうなTips

  • ComposeのCanvasonDrawの中で呼んだ動きの挙動について疑問点が生まれたら...
    • LayoutNodeDrawScopeのそのメソッドの実装を確認する(それが呼ばれている)
    • LayoutNodeDrawScopeにその実装がなかったらCanvasDrawScope のそれを確認する(それが呼ばれている)
    • その時、CanvasDrawScopecanvasAndroidCanvasになっている。
    • さらにそのcanvasinternalCanvasはViewGroupの1つであるであるAndroidComposeViewdispatchDrawに渡ってきている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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2