【JetpackCompose】”いい感じの”カウントアップアニメーションを作ってみた

## やりたいこと/できること
Webサイトなどでよく見かける「加速したあと減速するカウント(下記動画を見ていただいたほうが分かりやすいと思います)」をAndroidでも実装する方法について説明します。
サンプルとして、ボタンを押すと各Interpolator(下記解説)に応じたカウントアップアニメーションを開始する内容を実装します。
![sample_resize.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/45ce57c7-5733-88c0-3587-52e44126f0cd.gif)


### 環境
material3 : 1.0.0-beta03
Kotlin : 1.7.0
JetpackCompose : 1.2.0

## ソースコードと説明

### ValueAnimationを使う

Androidには様々なアニメーションの種類がありますが、値の増減をアニメーションさせるためにはValueAnimationを使います。ValueAnimationではint、float、または color の値のセットを指定することで、アニメーションの継続時間中に値の増減にアニメーションをセットすることができます。

```kotlin
@Composable
fun CountUpScreen() {
    val initValue = 0
    val targetValue = 100
    val setDuration = 10000L // アニメーションの時間を10秒に設定
    val count = remember{ mutableStateOf(0) }

    val animator = ValueAnimator.ofInt(initValue, targetValue).apply{
        duration= setDuration
        interpolator= AccelerateDecelerateInterpolator()
        addUpdateListener{
            count.value =it.animatedValue as Int // アニメーションで更新された値をcountへ格納
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ){
        Text(
            text = "${count.value}",
            fontSize = 96.sp,
            fontWeight = FontWeight.Bold
        )
        Button(
            onClick ={animator.start()} // ボタンを押したらアニメーションスタート
        ){
            Text(text = "CountUp")
        }
    }

}
```

今回のサンプルでは以下のメソッドを使用しました。
- `ofInt()`：アニメーションで開始する値と終了値を設定
- `dulation`：初期値から終了値へ変化するまでにかかる時間
- `interpolator`：値の変化に対するアニメーションのタイプ(詳しくは以下の項目で解説)
- `start()`：アニメーションの実行
- `onAnimationUpdate()`：アニメーションで更新した値を取得し、countへ格納
詳細な内容についてここでは割愛していますので、[こちらのリファレンス](https://developer.android.com/reference/android/animation/ValueAnimator)をご参考ください。

### Interpolatorでアニメーションの変化を制御する

アニメーションの変化を制御するにはInterpolatorを指定します。

デフォルトでは`AccelerateDecelerateInterpolator()`が設定されており、以下の表に示したInterpolatorを設定することで、アニメーションに様々な変化をつけることができます。

| 動作 | クラス/インターフェース | 説明 |
| --- | --- | --- |
| ![AccelerateDecelerateInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/1ce02d62-fd20-5164-6967-06b31dbe4036.gif) | [AccelerateDecelerateInterpolator](https://developer.android.com/reference/android/view/animation/AccelerateDecelerateInterpolator) | 開始から加速していき途中で減速 |
| ![AccelerateInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/77fb668c-9772-477d-5d6b-9f376b1448ad.gif)| [AccelerateInterpolator](https://developer.android.com/reference/android/view/animation/AccelerateInterpolator?hl=ja) | ゆっくりと開始し、その後に加速 |
|![Anticipatepolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/96a2bef5-252f-5838-38a5-c01a87efab8b.gif) | [AnticipateInterpolator](https://developer.android.com/reference/android/view/animation/AnticipateInterpolator?hl=ja) | 変化が逆方向に開始してから、順方向に切り替わる |
|![AnticipateOvershootInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/0536135f-8bdb-6c46-19df-60376da0b47e.gif)| [AnticipateOvershootInterpolator](https://developer.android.com/reference/android/view/animation/AnticipateOvershootInterpolator?hl=ja) | 変化が逆方向に開始し、順方向に切り替わり、目標値を過ぎてから終了値に戻る |
|![BounceInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/77f83a90-7e9a-b70c-c75e-c9b48dfd0d5b.gif)| [BounceInterpolator](https://developer.android.com/reference/android/view/animation/BounceInterpolator?hl=ja) | 終了値に向かってバウンドする（ボールが跳ね返るイメージ） |
| ![CycleInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/1a9193f4-bb5b-5265-2862-2d5068615a5e.gif) | [CycleInterpolator](https://developer.android.com/reference/android/view/animation/CycleInterpolator?hl=ja) | 指定したサイクル数だけ逆方向と順方向に切り替わる |
| ![DecelerateInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/5c0635a4-09a9-77c8-acf4-8dfe0ff7bd06.gif) | [DecelerateInterpolator](https://developer.android.com/reference/android/view/animation/DecelerateInterpolator?hl=ja) | 変化が急速に開始し減速 |
|![LinearInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/bf80cd82-1ced-3763-c7b5-8a46d06fd358.gif)| [LinearInterpolator](https://developer.android.com/reference/android/view/animation/LinearInterpolator?hl=ja) | 変化の速度が一定 |
|![OvershootInterpolator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/059aff1f-1783-10fd-6396-e204242ecfd5.gif)| [OvershootInterpolator](https://developer.android.com/reference/android/view/animation/OvershootInterpolator?hl=ja) | 変化が順方向に開始し、最終値を過ぎてから戻る |

カウントアップ/ダウンでよく使うのは
- `AccelerateDecelerateInterpolator()`
- `AccelerateInterpolator()`
- `DecelerateInterpolator()`
- `LinearInterpolator()`

あたりでしょうか🤔

## まとめ

- 値の変化にアニメーションをつけるにはValueAnimationを使う
- Interpolatorを指定することで様々なアニメーションへカスタマイズが可能

今までAndroidのアニメーションはMotionLayoutしか使ったことがなく、今回このサンプルを作るにあたって初めてアニメーションについて調べるキッカケになりました。今回はValueAnimationという昔からある技術を使いましたが、今後はJetpackComposeで提供されているアニメーションAPIを使って様々なアニメーションにチャレンジしてみたいと思います。

**おまけ**

文字のアルファ値もアニメーションして終了値に向かうに連れ濃くなるようにしてみました。（診断アプリとかに使えそう？）
![omake.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78138/62a5ae2b-ecb4-10d4-e23c-151ca9a7efd5.gif)

## 参考

### 関連ドキュメント

- ****プロパティ アニメーションの概要****
https://developer.android.com/guide/topics/graphics/prop-animation?hl=ja
- **ValueAnimator**
https://developer.android.com/reference/android/animation/ValueAnimator