SpringAnimationを気軽に使おう


はじめに

ZOZOテクノロジーズ #2 Advent Calendar 2019 11日目の記事になります。

この記事では今年のGoogle IOのセッションにあった Motional Intelligence: Build Smarter Animations (Google I/O'19) を見て、「SpringAnimationって意外と気軽に使えるのでは?」と感じ実装してみた内容について記述します。

これを読んだ人が同じように SpringAnimation に可能性を見出してくれたら幸いです。
記事のボリュームを抑えるために SpringAnimation についての解説は省略します。
詳しく知りたい方は最後の『参考』にいくつかリンクを記載しておくのでそちらをご確認ください。

実装するアニメーションイメージ

「汎用的に使えるよ」ということを言いたいのでシンプルなものを題材にしたいと思いました。
そこで思いついたのが、ソシャゲでよくある?能力アップ用アイテムが確率で成功したり失敗したりするといったアニメーションです。

大きくなったら強化成功で、最初のサイズに戻ったら強化失敗を表現しています。

アニメーションの仕様

  1. 「大きくする(成功)」または「大きくする(失敗)」をタップするとアイコンを5倍にするアニメーションが開始します。
  2. アニメーション開始から0.5秒後に成功・失敗いずれかに合わせてアニメーションを変更します
  3. 成功時はそのまま5倍にし、失敗時は元のサイズに戻します

ロジックは以下のような感じ

ViewModel
    fun clickSuccessButton() {
        _scale.value = 5.0f
        _result.value = "実行中..."

        viewModelScope.launch {
            // 擬似API通信
            delay(500)

            _scale.value = 5.0f
            _result.value = "成功"
        }
    }

    fun clickFaultButton() {
        _scale.value = 5.0f
        _result.value = "実行中..."

        viewModelScope.launch {
            // 擬似API通信
            delay(500)

            _scale.value = 1.0f
            _result.value = "失敗"
        }
    }

_scale_result もLiveDataです。 _scale の値が変更されるとアニメーションが開始するようにActivityで実装します。

アニメーションの実装(ViewPropertyAnimator)

ここから本題です。
まずは SpringAnimation を使わずにシンプルに ViewPropertyAnimator で実装してみます。

Activity

        val image1: ImageView = findViewById(R.id.droid1)
        // アニメーション中にアニメーションが変わるようにしたかったので1秒間のアニメーションにしています
        image1.animate().duration = 1000
        viewModel.scale.observe(this, Observer {
            image1.animate().scaleX(it)
            image1.animate().scaleY(it)
        })

        val successButton: Button = findViewById(R.id.successButton)
        successButton.setOnClickListener {
            viewModel.clickSuccessButton()
        }

        val faultButton: Button = findViewById(R.id.faultButton)
        faultButton.setOnClickListener {
            viewModel.clickFaultButton()
        }

scale に変更があった場合にアニメーションを開始します。
しかしながらこれを実行すると冒頭に貼り付けた動画のようにはなりません。

成功も失敗もどちらもアニメーションが上書きされる段階(0.5秒経過後)で一瞬かくついているように見えます。

ViewPropertyAnimatorではなぜかくつくのか

アニメーション実行時に開始値を指定せず終了値だけを指定しているので連続性は保てています。
ただし上の実装では、新しいアニメーションは古いアニメーションをキャンセルしているだけなので、キャンセルしたところから1秒かけてアニメーションを再開しちゃっています。
これがかくつく原因です。当然スムーズとは言えないですね。

そこで SpringAnimation の出番です。

アニメーションの実装(SpringAnimation)

Google IOのセッションでも話があったのですが SpringAnimation を気軽に扱うには下準備が必要です。
これから記載する下準備はGoogleが公開しているソースコードにちょこっと手を加えたものです。
(末尾の『参考』にGoogle Developers Japanの記事を貼っておきました。正しいコードはその記事内からアクセスできるのできちんとしたものが気になる場合はそちらからご確認ください。)

下準備

SpringAnimationViewPropertyAnimator のように実行中のアニメーションを上書きするような仕組みはないので拡張関数を用意します。

fun View.spring(property: DynamicAnimation.ViewProperty): SpringAnimation {  
    val key = getKey(property)
    var springAnim = getTag(key) as? SpringAnimation?
    if (springAnim == null) {
        springAnim = SpringAnimation(this, property)
        setTag(key, springAnim)
    }

    // アニメーションの時間をViewPropertyAnimatorと同じくらいにしたかったので関連する値を調整
    springAnim.spring = (springAnim.spring ?: SpringForce()).apply {
        this.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
        this.stiffness = 20f
    }

    return springAnim
}

このなかの getKey(property) についても下準備として用意する必要があります。(詳しくは本家の記事をご確認ください)
また、パラメータの調整は私の方で独自に追加したものなのであまり重要ではありません。

こういった関数を用意すると ViewPropertyAnimator の時のように実装できます。

実装


        val image1: ImageView = findViewById(R.id.droid1)

        viewModel.scale.observe(this, Observer {
            image1.spring(SpringAnimation.SCALE_X).animateToFinalPosition(it)
            image1.spring(SpringAnimation.SCALE_Y).animateToFinalPosition(it)
        })

        val successButton: Button = findViewById(R.id.successButton)
        successButton.setOnClickListener {
            viewModel.clickSuccessButton()
        }

        val faultButton: Button = findViewById(R.id.faultButton)
        faultButton.setOnClickListener {
            viewModel.clickFaultButton()
        }

ViewPropertyAnimator の時に負けないほどシンプルです。
結果は冒頭と同様です。(一応同じ動画貼っておきます。)

それぞれのアニメーションを二つ並べるとこんな感じです。

左側が ViewPropertyAnimator で、右側が SpringAnimation になります。
どっちが良いかは一目瞭然ですね。

SpringAnimationの欠点

Google IOのセッションを見ていた時は全然気づかなかったというか知らなかったのですが、今回実際に実装してみたところやはり欠点がありました。
それは アニメーションの時間が指定できない というところです。
ViewPropertyAnimator との比較をするために同じ時間を設定しようとしたところ、 duration の指定ができないことに気づきました。
おそらくバネの強さとか初速とか変動値によってスピードや動きが変わるので duration をシンプルに設定するのはできないのかなと
ただ、逆にそれら関連するパラメータを完全に理解できればある程度は duration の指定も可能かもしれません。
なのでもし時間があれば今度はそれについて調べて記事にできたらなと思います

まとめ

下準備の若干の面倒さと、durationが指定できない問題を除けば SpringAnimation は気軽に利用できてわかりやすい効果をもつアニメーション実装方法だと思います。
慣性なんか使うアニメーションなんて限られていると思っていたのですが、実装もシンプルなので基本的なアニメーション(移動とか拡大とか回転とかアルファとか)はこれで実装しておくのが良いのではと思います。
きっとその方が完成した時にスムーズに見えるはずです。
今思いついたのだとお気に入り登録とかによく使うハートが点灯するように見えるアニメーションとかにも使えそう。

そしてところどころで記載していますが、本記事はGoogle IOのセッションを参考に作成したものなのでより詳細が気になる場合は参考にリンクを用意したのでそちらをご確認ください。

参考