SpringAnimationを気軽に使おう

# はじめに
ZOZOテクノロジーズ [#2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech2) 11日目の記事になります。 

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

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

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


大きくなったら強化成功で、最初のサイズに戻ったら強化失敗を表現しています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/b008e328-b017-3106-efd5-4511d64bbb44.gif" width="300px" />


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

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

```kotlin: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` で実装してみます。

```kotlin: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` に変更があった場合にアニメーションを開始します。
しかしながらこれを実行すると冒頭に貼り付けた動画のようにはなりません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/e2d51d4c-e7f7-ee4c-7e8f-e249f800da2a.gif" width="300px" />
成功も失敗もどちらもアニメーションが上書きされる段階（0.5秒経過後）で一瞬かくついているように見えます。

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

そこで `SpringAnimation` の出番です。

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

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

```kotlin
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` の時のように実装できます。

## 実装

```kotlin

        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` の時に負けないほどシンプルです。
結果は冒頭と同様です。（一応同じ動画貼っておきます。）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/b008e328-b017-3106-efd5-4511d64bbb44.gif" width="300px" />

それぞれのアニメーションを二つ並べるとこんな感じです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149859/63c90eda-99c3-02a6-eaf1-11689072d850.gif" width="300px" />
左側が `ViewPropertyAnimator` で、右側が `SpringAnimation` になります。
どっちが良いかは一目瞭然ですね。

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

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

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

# 参考
* [SpringAnimation | Android Developers](https://developer.android.com/reference/android/support/animation/SpringAnimation)
* [Motional Intelligence: Build Smarter Animations (Google I/O'19)](https://www.youtube.com/watch?v=f3Lm8iOr4mE)
* [Google Developers Japan: モーショナル インテリジェンス: スマートなアニメーションを作成する](https://developers-jp.googleblog.com/2019/10/motional-intelligence.html)
* [Spring Animationの動きを理解して使いこなす](https://techblog.zozo.com/entry/android-spring-animation)