JetpackComposeを利用して状態に寄らないアニメーションを実装する


こんにちは！
Life is Tech! 名古屋土曜日スクールでクラスマネージャーをしているカーキです。
本日、アドベントカレンダーの９日目を担当します。
普段はAndroidエンジニアとして、働いているのでAndroidに関するネタを投稿します。

## 概要

2年前にも[Androidでポヨンするアニメーション](https://qiita.com/Takarkiz/items/5af59902b042171d21dd)を実装する紹介をしたのですが、今回はその2022年版として、JetpackComposeだとどう実装をするのかを想定して紹介していきます。

具体的には、以下のプリン画像をタップして、ポヨヨンとアニメーションする動きを実装していきます。
![purin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/00665b37-45c1-6e47-e472-6915eb886098.png)

## ゴール
今回のゴールとしては、2年前に投稿した[Androidでポヨンするアニメーションを実装する](https://qiita.com/Takarkiz/items/5af59902b042171d21dd)で発見した、最もポヨンらしさを表現できるパラメータをそのまま利用してJetpackComposeで同じ動きを目指していきます。

![poyon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/ebf9893a-590c-687d-a458-b68a5dcd3048.gif)
👆こんな感じ

当時は`View`の**scale**という大きさの比率を示すプロパティをアニメーションを用いて変更させることでポヨンを実現していました。

![ぽよよん.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/2d1765ca-72b7-047e-0734-1eb6cdbcc215.jpeg)

アニメーションの時間経過とその挙動を示したのが上の図です。
タップされた直後にスケールが`0.6`となり、バウンスをしながら元のスケールの値である`1.0`に戻っていきます。

またバウンスのアニメーションにはAndroidxの`dynamicanimation`にある[Springアニメーション](https://developer.android.com/guide/topics/graphics/spring-animation)を利用していました。Springアニメーションではその名の通りばねの物理法則をアニメーションに利用したアニメーション方法になります。詳細は当時の記事を読んでもらえばと思いますが、理想的な**ポヨン**を実現するパラメータとして以下の値を利用していました。

```kotlin
dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
stiffness = SpringForce.STIFFNESS_LOW
```

## 表示する

まずはJetpackComposeでプリンを表現できなければ、何も始まりません。
`Image`のコンポーネントを利用して表示していきます。

```Poyon.kt
@Preview
@Composable
fun Poyon() {
    Image(
        modifier = Modifier
            .size(40.dp)
            .scale(1.0f),
        painter = painterResource(id = R.drawable.sweets_purin),
        contentDescription = null
    )
}
```

プレビュー結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/2b54e124-379e-a75a-9fe8-803438afdee7.png" width="400">

これでプリンが表示できました。
すぐにプレビュー結果を見ることができるので、安心感がありますね😀

## アニメーションを考える
ここからどうポヨンのアニメーションを実装していくのかを考えていきます。

JetpackComposeでアニメーションを実装していく上で、[公式のドキュメント](https://developer.android.com/jetpack/compose/animation?hl=ja#overview)がとても参考になるのでみてみましょう。

![スクリーンショット 2022-12-27 0.16.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/dcb85b4f-c40a-28a9-251a-9fa941cd708f.png)

このようなアニメーションを実装する際に何を使用すると良いのかがわかるフローチャートが載っています。
今回のポヨンに関してはViewの状態変化によらないアニメーションになるので、`Animatable`が該当します。

JetpackComposeのアニメーションでは高レベルアニメーションとして状態変化に応じたアニメーションを実装するAPIが多く用意されており、ドキュメントも充実しています。今回は`Animatable`という低レベルアニメーションを利用するため、自身でCoroutineScopeを用意する必要があります。高レベルアニメーションを駆使することで実現は可能ですが、無駄な状態変化を生み出してしまうことになるので、`Animatable`で実装をするのが良いと思います。

## まず小さくする

`Animatable`では、変化対象の値を`Animatable`でラップして利用します。
そしてCoroutineScopeの内部でアニメーションによる値の変化を加えていきます。

```Poyon.kt
val coroutineScope = rememberCoroutineScope()
val scale = remember {
    Animatable(1f)
}
```

上記で指定したスケールをプリンに適応させつつ、タップ時にはまず`0.6`のスケールになって欲しいのでそのアニメーションを加えてみます。

```Kotlin
Image(
    modifier = Modifier
        .size(40.dp)
        .scale(scale.value)
        .clickable(
            onClick = {
                coroutineScope.launch {
                    scale.animateTo(
                        0.6f,
                        animationSpec = tween(durationMillis = 0)
                    )
                }
            }
        ),
    painter = painterResource(id = R.drawable.sweets_purin),
    contentDescription = null
)
```

最初に`0.6`のスケールとなるよう`tween`アニメーションを追加しています。
`tween`では指定した遅延時間の中でEasingしながらアニメーションするものになりますが、遅延時間を`0msec`と指定しているため、一瞬でスケールが`0.6`となります。

このアニメーションの結果がこちらです。
![小さくなるプリン.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/f0865ba1-20de-b7f3-915e-bbac92be4002.gif)

タップした瞬間にスケールが`0.6`になっているのが確認できます。

## 大きさを戻す
小さくするところまでできたので、次は大きさをバウンスさせながら戻していきたいです。
同じように`Animatable`を使って実装していきます。

アニメーションの動きの詳細であるAnimationSpecには`spring`アニメーションが用意されているのでそれを利用していきます。
`Image`全体の実装が以下のようになります。

```Kotlin
Image(
    modifier = Modifier
        .size(40.dp)
        .scale(scale.value)
        .clickable(
            onClick = {
                coroutineScope.launch {
                    scale.animateTo(
                        0.6f,
                        animationSpec = tween(durationMillis = 0)
                    )
                    scale.animateTo(
                        1f,
                        animationSpec = spring(
                            dampingRatio = Spring.DampingRatioMediumBouncy,
                            stiffness = Spring.StiffnessLow,
                        )
                    )
                }
            }
        ),
    painter = painterResource(id = R.drawable.sweets_purin),
    contentDescription = null
)
```

JetpackComposeとAndroidViewであっても、同じAndroidの世界なのでSpringアニメーションの指定の方法が同じなのは助かりますね。

![スプリング.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82263/cdea5d04-9d62-6001-770d-6e750744d0bd.gif)

アニメーションはこんな感じで、しっかりポヨンしています🍮

今回のコンポーネント全体のコードは以下のようになります。

```Poyon.kt
@Preview
@Composable
fun Poyon() {

    val coroutineScope = rememberCoroutineScope()
    val scale = remember {
        Animatable(1f)
    }

    Image(
        modifier = Modifier
            .size(40.dp)
            .scale(scale.value)
            .clickable(
                onClick = {
                    coroutineScope.launch {
                        scale.animateTo(
                            0.6f,
                            animationSpec = tween(durationMillis = 0)
                        )
                        scale.animateTo(
                            1f,
                            animationSpec = spring(
                                dampingRatio = Spring.DampingRatioMediumBouncy,
                                stiffness = Spring.StiffnessLow,
                            )
                        )
                    }
                }
            ),
        painter = painterResource(id = R.drawable.sweets_purin),
        contentDescription = null
    )
}
```

## 最後に
JetpackComposeのアニメーションは様々な種類が事前に用意されており便利ですが、状態変化によらないアニメーションとなるとどうするんだ？と内心思っていました。
それでも`Animatable`を利用することでサクッと実装できてしまって感動しました。
やっぱりJetpackCompose良いですね

余談ですが、プリンの手話可愛いですよね
それは、皆さんも楽しいポヨンライフを🍮

## 参考

- Android Developer Jetpack Compose アニメーション
    - https://developer.android.com/jetpack/compose/animation?hl=ja#overview

- Android Button onClick Animation Using Jetpack Compose
    - https://semicolonspace.com/android-jetpack-compose-button-onclick-animation/