たくさんのTextViewをアニメーションさせる
画面に20個ぐらいアニメーションするビューがあってなんとかして60fps出すためにいろいろやったことをメモしておく。
エミュレーターでやってたのが全部悪い(実環境で動かしたらわりとすんなり出た)というオチなので参考になるかはわからない。
前提
ValueAnimator
でAnimatable
なDrawable
をKeyFrame
で波紋のようなアニメーションを周期的に動かす。これはせいぜい数個ValueAnimator
でTextView
のtextColor(白と黒の間のグレースケール)を周期的に変えていく。これは20個ぐらいある
方針
Debug.startMethodTracing()
で数フレーム分観測し、いらない処理を見つけてこれが実行されないように削っていく。この資料の38Pから数ページが参考になる。
AndroidVitals徹底活用 - Speaker Deck
最初のほうはGCも頻繁に起きていたのでAndroid Studioのメモリアナライザも使ったりした。
ポイント
ValueAnimator
でアニメーションをやるときは次のメソッドを実装することになると思うが、この中の処理を軽くしたり、中で新しいオブジェクトを作らないようにする。
TypeEvaluator.evaluate()
- SDKのクラスを使うときは中で何をやっているか確認する。例えば
ArgbEvaluator
は最初にセットしたColor Intがevaluate()
に渡される時にはInteger
のオブジェクトになってしまう。
- SDKのクラスを使うときは中で何をやっているか確認する。例えば
AnimatorUpdateListener.onAnimationUpdate()
Drawable.draw()
やったこと
オブジェクト生成の抑制
listOf()
とかarrayOf()
とかで作っていたKeyFrame
のvalueの配列をfloatArrayOf()
にする。これによりTypeEvaluator
に値を渡すときのFloat
オブジェクトの生成を抑制できるFloatTypeEvaluator
にキャッシュ用の配列を渡して初期化する。これをしないとフレームごとにfloat[]
が作られてしまう- 配列操作の処理でmapとかfilterを使っているところをfor文に置き換える。
FloatArray
でmapとかを呼ぶとIterator
がフレームごとに作られてしまうしnext()
がちょっと気になるくらい遅い。また、Javaコードにデコンパイルしてコードを見ると、mapに渡した関数がクラスになっていてこれのオブジェクトも毎フレーム生成されていた。
描画系
- すべての
TextView
の背景にAnimatable
なDrawable
をセットして、動かすときになったら表示させるようにしていたがこれをやめて、アニメーションさせるときにはじめてセットする。Drawable
がセットされていたら更新しようとするのでこれをやめさせる。そこそこ効いた。 WRAP_CONTENT
なTextView
にGravity.CENTER
を指定していたのをはずす。テキストアライメントがNORMAL以外だとBolingLayout.draw()
の中で毎回描画領域の再計算が行われてしまうのでこれをやめさせる。NORMALになっていると即座にCanvas.drawText()
が実行される。そこそこ効いた。- ヒントテキストもリンクも必要ないので、
hintTextColor
とlinkTextColor
にnullをセットしておく。これをやっておくとTextView.setTextColor()
をやった時にhintTextColor
とかlinkTextColor
も更新しようとするのを防げる。超細かい。 TextView.setTextColor(int)
ではなくTextView.setTextColor(ColorStateList)
のほうを使う。ColorIntを渡すほうは内部的にColorStateList.valueOf(int)
を呼んでColorStateList
に変換しているのだが、内部的にsynchronized
で囲われているせいか時々めっちゃ遅いことがある。グレースケールなら長さ256の配列に収まるのでアプリ起動時に作ってこれを使うようにする。超細かい。- 同じタイミングで動く複数の
Drawable
を別々のValueAnimator
で制御していたが、これをやめて一つのValueAnimator
のなかで全部のDrawable
のinvalidateをやることにした。結構効いた。 Drawable.invalidateSelf()
をやめてView.postInvalidateInAnimation()
を使うようにした。そこそこ効いた。
カジュアルにIdlingResourceを使う
- UIテストをやる時にMockWebServerを使ってサーバの挙動をモックしていると、レスポンスをちゃんと待ち構えないとテストが失敗することが稀によくある*1
- カジュアルに
IdlingResource
を使ってカジュアルに待てるようにしたい - テストを書くためのハードルを下げたい
ポイント:
- ある状態になるまで待ちたくなった時にregisterして所望の状態になったらすぐにunregisterする。そうせず大域的に使うといろんな条件がバッティングして失敗する。
- registerしたらなにがなんでもunregisterされるようにしたいのでtry (catch) finallyでくくる
isIdleNow()
のなかでActivityLifecycleMonitor.getActivitiesInStage(Stage)
を使ってactivityを取得できれば勝確ActivityScenario
とかActivityTestRule
とかをわたしてもよいかも?- 結構無茶なことをしているし
IdlingThreadPoolExecutor
とかを使ってうまくやるのがいいと思う - ダイアログとか
RecyclerView
の状態を待ちたいときはこうするしかないような気もするがそもそもこんなことしなければならないのが間違っている気がする
コード例:
まずはIdlingResources
。結局はisIdleNow()
の挙動を実装できればいい。公式サイトにはisIdleNow()
の中でIdlingResource.ResourceCallback.onTransactionToIdle()
を呼ぶなと書いてあるが今回の用途ではここで呼ばないとテストが進まなくなることがあるので仕方なく呼ぶ。
fun createIdlingResource(name: String, block: () -> Boolean): IdlingResource { return object : IdlingResource { override fun getName() = name override fun isIdleNow(): Boolean { val isIdle = block() if (isIdle) { callback?.onTransitionToIdle() } return isIdle } private var callback: IdlingResource.ResourceCallback? = null override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) { this.callback = callback } } } fun waitWithIdlingResource(name: String, block: () -> Boolean, afterTask: () -> Unit) { val idlingRegistry = IdlingRegistry.getInstance() val resource = createIdlingResource(name, block) try { idlingRegistry.register(resource) afterTask() } finally { idlingRegistry.unregister(resource) } }
先ほどのメソッドを作ってテスト対象のActivityが所定のStageにIdlingResourceを作ってみる。
inline fun <reified T : Activity> waitForActivity(stage: Stage, noinline afterTask: () -> Unit) { val name = "wait_for_${T::class.java.simpleName}_in_${stage.name}" waitWithIdlingResource(name, { ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull { it is T } != null }, afterTask) }
先ほどのwaitForActivityをリファクタリングしてFragmentが差し込まれるまで待つIdlingResourceを作ってみる。
inline fun <reified T : Activity> waitForActivity( stage: Stage = Stage.RESUMED, name: String = "wait_for_${T::class.java.simpleName}_in_${stage.name}", crossinline onActivity: (T) -> Boolean = { true }, noinline afterTask: () -> Unit ) { waitWithIdlingResource(name, { val a = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull { it is T } ?: return@waitWithIdlingResource false return@waitWithIdlingResource onActivity(a as T) }, afterTask) } inline fun <reified T : Fragment> waitForFragment(noinline afterTask: () -> Unit) { waitForActivity<FragmentActivity>(name = "wait_for_${T::class.java.simpleName}", onActivity = { a -> a.supportFragmentManager.fragments.firstOrNull { it is T } != null }, afterTask = afterTask) }
しなくていいならしないに越したことはないと思うけど、退っ引きならない事情でどうしてもやらなければならない時だけどうぞ。Activityを待つ奴を応用するとRecyclerViewに所望のViewが差し込まれるまで待つとかいうのも作れるようになるので興味のある方は挑戦してみてください。
最近読んだのでよかったらどうぞ。
*1:結構すぐ返ってくるので待ってなくても割と成功する
Androidでマルチモジュールをやってみているという話
droidkaigiでマルチモジュール関連の発表をたくさん聞いて、やってみたくなったのでやってみている。
データレイヤーを単に外に出すところまではなんとなくできたけどそこから先のところでうまく行かなかったり腑に落ちたり落ちなかったりしている。基本的な設計力がないからマルチモジュール化できないということなのであろうか。どうしてそうなっているかを理解せず何かの真似でやってみてもうまく行かない。
ちなみにここでモジュールって言ったらGradle(とかKotlin)で言う所のモジュールのことです。
daggerがなんか難しい
- モジュールとSubcomponentとを対応させればいいんじゃね?と考えて公式ページを見ながらやってみるものの説明の通り(?)にやってみたらStackOverflowError
- ComponentをもっているモジュールがSubcomponentの依存関係に引っ張られて本来不要なモジュールに依存せざるを得なくなっている*1
- kaptの挙動がよくないという説がある様子
- apiのモジュールを挟めばいいんだろうか
- koinとかはどうなんだろうと思い始める
スタイルとかテーマって開発中どうやって確認すればいいんだろう
- マテリアルデザインのテーマが読み込めなくてFABを表示できない(?)何が悪いのかもよくわかっていない
- styleとかthemeはsharedなモジュールに置く?スタイルと実装とは切り離して考えるべきだとも思うけどそこまでやるのもなんか違う気がしている。
Activity, Fragment, ViewModelはどう分けるのがよいのか
- ViewModelはViewに依存しないとはいっても、どんなViewDataBindingにバインドされるかは知っているはず*2ではないかと思うので、レイアウトファイルとViewModelはなんとなくセットにしておきたい
- なんだかんだ言って画面はFragmentを組み合わせて作ることになるので、Fragmentのレイアウトファイルと、それとバインドするViewModelとをセットにして作ることになるはず
- そう考えるとFragmentとViewModelは一緒のモジュールに入れればいいんじゃないのか?と思わなくもない。でもモジュール化の意義的に正しいのか、なんとなく違和感を覚える*3
- 一方で、ActivityのレイアウトファイルはFragmentのコンテナしか置かないとかいうやつがでてくる*4
- そういうやつはバインドするデータやビューがあんまりないのでViewModelとバインドできてもおいしくない
- そういうActivityはFragmentの置き換えを実装するだけでいいという感じになる。どんな時にどのFragmentを置くかというのをViewModelで管理することになるかもしれないが、FragmentManagerをいじるロジックはActivityに書くことになり画面が増えてくるとつらい
- Fragmentの置き換えを専門にやるクラスがあればよいのか?→JetpackのNavigationがそれにあたるか
- navigationを使えば、ActivityやFragmentは他にどんなActivityやFragmentがあるのか知らなくてよさそう
- navigationだけがActivityやFragment、他のnavigationを知っていればよいというふうにできれば、navigation(の具象クラス)はapplicationと同じモジュールに置けばよい
- 画面のことはenumとかsealed classで定義したクラス(sharedなモジュールに置く)で表現すれば、navigationの抽象クラスしか知らないクラスの中でも画面遷移を実現できそう
- 画面遷移イベントの発行を行う処理はViewModelでやってよい
- 画面遷移を依頼するようなイベントオブジェクトを定義するとMVIになるのか?
先は長い。俺たちの戦いは始まったばかりだ。