【Jetpack Compose】Scaleアニメーション

ComposeをScaleさせるアニメーションの実装に苦労したので書きます。

ここで書くのは以下2パターンです

- 無限ループするScaleアニメーション
- 表示時に一回だけ実行するScaleアニメーション

今回動かすComposeはこんな丸いBoxです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166629/01a82c84-d5f0-38c4-463c-5a2626464157.png" width=320 />

```kotlin
@Composable
fun Circle(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .size(80.dp)
            .clip(CircleShape)
            .background(Color.DarkGray)
    )
}
```

## 無限ループするScaleアニメーション

無限に実行するほうが簡単でした。
基本的にJetpack Composeのアニメーションはmodifierの値を変化させることでアニメーションさせます。今までのAndroid ViewでやっていたようなScaleアニメーションとは違いますね。

そもそもScaleさせるには、BoxのModifierにてscale()を呼び出します。

```kotlin
// 80dpのサイズから1.2fスケールしてる
Box(modifier = Modifier.size(80.dp).scale(1.2f))
```

この指定の方法だと最初から1.2fスケールしている状態になっているだけです。
ようはこの値をアニメーション変動させればアニメーション表示されるという仕組みです。

```kotlin
val infiniteTransition = rememberInfiniteTransition()

val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 1.3f,
    animationSpec = infiniteRepeatable(
        animation = tween(800),
        repeatMode = RepeatMode.Reverse
    ),
)

```

で、上のコードがアニメーションFloat値を用意するコードです。これで全文です。
無限に実行するようのinfiniteTransitionが用意されているので、それをまず用意します。
Composeにて状態を保持する必要があるので `rememberInfiniteTransition()` でtransitionを生成していますが、まあ定形だと思って使ってます。

このinfiniteTransitionからanimateFloatでアニメーションするFloat値を用意します。
`initialValue` で初期値を指定して、 `targetValue` で変化する値を指定します。

animationSpecでアニメーションの挙動を指定することができます。今回はスケールアニメーションなので、大きくなったあと小さくする必要があるので、`infiniteRepeatable` を指定します。

この指定をBoxのModifierに指定します。

```kotlin
@Composable
fun Circle(modifier: Modifier = Modifier) {
    val infiniteTransition = rememberInfiniteTransition()

    val scale by infiniteTransition.animateFloat(
        initialValue = 1f,
        targetValue = 1.3f,
        animationSpec = infiniteRepeatable(
            animation = tween(800),
            repeatMode = RepeatMode.Reverse
        ),
    )

    Box(
        modifier = modifier
            .size(80.dp)
            .scale(scale)
            .clip(CircleShape)
            .background(Color.DarkGray)
    )
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166629/fa3bc560-4897-f87a-eb6d-89fc1eea198c.gif" width=320 />

## 表示時に一回だけ実行するScaleアニメーション

こちらはだいぶ調べて苦戦しました。。
現状Jetpack Composeのアニメーションには、一回だけ実行する的な簡易なものはなさそうです。infinitのときと同じようにプロパティを変化させる必要があるのですが、一回だけ変化させるコードを自前で書く必要があります。

```kotlin
val scale by animateFloatAsState(
    targetValue = 1.3f,
    animationSpec = tween(
        durationMillis = 800
    ),
)
```

無限ループしない場合は `animateFloatAsState` を使えば良さそうなんですが、プロパティを見るとinitialValueが用意されていません。ドキュメントを見るとtargetValueに指定された値までの変動がアニメーションするのは同じなのですが、初期値は最初にセットされたtargetValueになるようです。
つまり、最初は1.0f、あとから1.3fにして1.0fに戻す処理が必要になります。

なんとも愚直な気がしちゃいますが、mutableStateOfで変数を用意してあげて状態管理します。

```kotlin
val playedAnimation by remember { mutableStateOf(false) }

val scale by animateFloatAsState(
    targetValue = if (playedAnimation) 1.3f else 1.0f,
    animationSpec = tween(
        durationMillis = 800
    ),
    finishedListener = { playedAnimation = false }
)
```

最初は1.0f、アニメーションを開始したら1.3fにして、アニメーションが終了したらfalseに戻して1.0fに戻すという寸法です。

さて、ここでもう一つ問題なのが、この `playedAnimation` をどこでtrueにしてあげるかとうことです。最初からtrueにしてしまっては、最初から1.3f表示されてしまってだめです。画面が表示されてからtrueにする必要があります。

そこでLaunchedEffectを使って変更します。
LaunchedEffectはComposeが構築された初回に実行する処理を書くことができる関数です。

```kotlin
@Composable
fun Circle(modifier: Modifier = Modifier) {
   var playedAnimation by remember { mutableStateOf(false) }

    val scale by animateFloatAsState(
        targetValue = if (playedAnimation) 1.3f else 1.0f,
        animationSpec = tween(
            durationMillis = 800
        ),
        finishedListener = { playedAnimation = false }
    )

    LaunchedEffect(true) {
        playedAnimation = true
    }

    Box(
        modifier = modifier
            .size(80.dp)
            .scale(scale)
            .clip(CircleShape)
            .background(Color.DarkGray)
    )
}
```

これで実行して見るといい感じに一回だけScaleアニメーションしています！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166629/fd9d68a8-6efc-c92b-fa31-6a39651b069f.gif" width=320 />

LaunchedEffectはCoroutineScopeで実行されるので、例えば表示を少し遅らせたいときにはdelay()も使えるので、そこらへんの制御も問題なさそうです。

```kotlin
LauncedEffect(true) {
  delay(1000) //1秒後にスケールする
  playedAnimation = true
}
```

ちなみになのですが、上記のアニメーションのコードをそこそこコード量のあるCompose内で書いていたらアニメーションがカクついていました。おそらくですが、rememberの値が高速に変更されて再構築が何度も走っているからだと思います。
そういう場合は、今回のCircle関数のように小さいComposableの中で書いてあげるといいと思います。

また、アニメーション指定を関数化することも一応できそうです。
すっきりしていいかもですね。

```kotlin
@Composable
fun Circle(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .size(80.dp)
            .scale(animateScaleFloat())
            .clip(CircleShape)
            .background(Color.DarkGray)
    )
}

@Composable
private fun animateScaleFloat(): Float {
    var playedAnimation by remember { mutableStateOf(false) }

    val scale by animateFloatAsState(
        targetValue = if (playedAnimation) 1.3f else 1.0f,
        animationSpec = tween(
            durationMillis = 800
        ),
        finishedListener = { playedAnimation = false }
    )

    LaunchedEffect(true) {
        playedAnimation = true
    }

    return scale
}
```