ということを納得するために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