開発中に遭遇して、けっこうハマったのでメモとして残しておきます。
TL;DR
- 一部の端末では開発者オプションを経由しなくてもアニメーションがオフにできるよ
- アニメーターに依存しているとバグの原因になるから、ValueAnimator#areAnimatorsEnabled()でチェックしよう
- カウントダウン処理はコルーチンでdelayと組みあわせてつくろう
Value Animatorが一瞬で終了してしまう
アプリケーションの中で、UIにカウントダウンするタイマーを表示して、タイマーが0になったら特定の処理を実施するという箇所がありました。 この箇所が一部のユーザーから一瞬で0になって処理が走ってしまうという不具合報告があり、コードを読んでもそれらしい不信な箇所がなくて困っていました。
元々のコード
ValueAnimator.ofInt(startMilliSec.toInt(), limitMilliSec.toInt()) .apply { addUpdateListener { val currentMilliSec = (it.animatedValue as Int) val remainingMilliSec = limitMilliSec - currentMilliSec if (remainingMilliSec == 0L) { TODO("Timeout process here") } } } .start()
実際にバグを手元で確認しましたが、ValueAnimator自体は動いているようでしたが、 初期の値が来て、次の一瞬で呼ばれたタイミングでいきなり上限の値が来て処理終了という判定になってしまっていました。
開発者オプションを見てみた
ここで、アニメーション関係の不具合という事で、まずは端末を確認して開発者オプションを見にいきましたが、 確認した時点で開発者オプション自体が表示されていませんでしたし、当然開発者オプションの中でもアニメーションの速度が変更になってりはしていませんでした。
Galaxy S系の一部の端末でアニメーションがオフにできる
開発者オプションを経由しないと、アニメーションをオフにしたりする事はできないだろうとおもっていたのですが、 どうも一部の端末では開発者オプションを経由しなくてもオフにする事ができてしまうようでした。
アニメーション機能が無効になっているかどうかを実行時にチェックする
そのような状況に対処するため、ValueAnimatorにアニメーションが有効になっているかどうかを確認するためのメソッドが生えています。 実際にオプションを有効にした上で確認してみたところ、アニメーションが無効になっているという判定になりました。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !ValueAnimator.areAnimatorsEnabled()) { Timber.e(IllegalStateException("Animator is disabled")) }
アニメーションに依存しないでコルーチンでカウントダウンする
viewLifecycleOwner.lifecycleScope.launch { while (true) { val remainingMilliSec = calcRemainingMilliSec() if (remainingMilliSec < 0) { TODO("Time-up process here") } delay(500) } }
ValueAnimatiorに依存してタイムアップ処理をするのは、不安定なためこのようにコルーチンでループとdelayを組みあわせて一定間隔で処理をするようにしています。 このようにした場合、Interporatorのようなクラスと組みあわせるのは自前でInterporatorのgetInterporationを呼びだしてやる事で可能かと思います。
まとめ
アニメーションでユーザーに表示してあげられる事に越した事はないですし、そういった時にAndroidの提供しているアニメーション系クラスは非常に便利です。 一方で、UIだけではなくカウントダウンして0になった○○のような処理に関しては、アニメーションに依存せずに適切に発火するべきなので、 そういった時にはコルーチンで実装すると安全でした。
また、アニメーションがオフになっているというケースが開発者にかぎらずAndroid端末では発生しうるため、コード上でアニメーションの有効無効を確認し 無効になっている時にどのようなUIになっているべきかという事をデザイナーさんと話しあえると良いのかなとおもいます。