個人開発のAndroidアプリにアニメーションを導入してみた【種族値クイズ】

# はじめに
こんにちは。新卒2年目のAndroidアプリエンジニアやってます。
なぐも[@southcloud_7960](https://x.com/southcloud_7960)というハンドルネームで活動しているものです。
今回は個人開発をしているアプリにアニメーションを導入してみたので、その経緯とできあがりについて紹介の記事を書こうと思います。

リリースしたアプリがこちらになります！

https://play.google.com/store/apps/details?id=com.nagumoapps.pokemonstatsquiz&pcampaignid=web_share

# この記事の対象
- AndroidアプリやiOSアプリを開発している人
- 個人開発をしている人
- アニメーションを使ったアプリの開発に興味がある人

# 開発しているアプリ
私が開発している種族値クイズ(1.1.0)のアプリがこんな感じです。
今回は詳細な説明を省きますが、以下のような基本機能を備えています。
- 問題の出題範囲を選ぶ
- 問題が出題される
- 入力して回答する
- ボタンを押して次の問題に進む
- ギブアップして答えを表示する

アプリ開発の詳しい経緯については、また別の記事でご紹介したいと思います！

# 開発しているアプリに関する悩み
リリースするために最低限の機能を詰め込んだので、ちょっと画面が寂しいです。
表示する種族値は文字だけですし、入力、正解の表示とアプリを使う流れが淡々としています。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/38e8d079-17f9-f6fa-791e-5085998b0fab.png">

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/aec83400-a1a1-fc95-ce13-84dec2bbd0a9.png">

そこでアニメーションを導入することにしました！

# アニメーションの導入
###  グラフの実装
```BaseStatsRowComponent.kt
@Composable
fun BaseStatsBarComponent(modifier: Modifier) {
    Canvas(modifier = modifier) {
        drawRect(
            color = Pink,
            topLeft = Offset(0f, 0f),
            size = size
        )
    }
}
```

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/09480fb3-23a8-f560-6e4c-317b61df089e.png">

まずはCanvasを使って、グラフを実装しました。
簡単な図形の描画にはCanvasがシンプルで使いやすいです。

### フラグでアニメーションを管理する
```BaseStatsBarComponent.kt
@Composable
fun BaseStatsBarComponent(modifier: Modifier) {
    val animationFlag = remember { mutableStateOf(false) }
    val animateWidth by animateFloatAsState(targetValue = if (animationFlag.value) 1f else 0f)

    Canvas(modifier = modifier) {
        animationFlag.value = true
        drawRect(
            color = Pink,
            topLeft = Offset(0f, 0f),
            size = Size(width = size.width * animateWidth, height = size.height)
        )
    }
}
```
animationFlagとanimateFloatAsStateを使ってアニメーションを管理します。
コンポーズされたタイミングでanimationFlagがfalseからtrueになり、
animateWidthで設定しているtargetの値が変更されることでアニメーションが走ります。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/95304e9a-5b7d-479c-c51b-64232aaf5be1.gif">

これでグラフがアニメーションするようになりました！

ただ、こちらのアニメーションの管理だと、次の問題へ進んだ時にアニメーションしてくれません。
falseがtrueになった時アニメーションするので、初回コンポーズ以降にアニメーションするタイミングがないからです。
trueになったまま変わりません。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/7b203206-9b41-f600-f4d5-835af80846c1.gif">

かといって、一度falseにするようなコードの場合…

```BaseStatsBarComponent.kt
@Composable
fun BaseStatsBarComponent(modifier: Modifier) {
    val animationFlag = remember { mutableStateOf(false) }
    val animateWidth by animateFloatAsState(targetValue = if (animationFlag.value) 1f else 0f)

    Canvas(modifier = modifier) {
        animationFlag.value = true
        drawRect(
            color = Pink,
            topLeft = Offset(0f, 0f),
            size = Size(width = size.width * animateWidth, height = size.height)
        )
    }
    animationFlag.value = false
}
```

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/38f93408-f9de-596d-0c45-405338f52d30.gif">

このように、ノリノリのグラフになってしまいました。
アニメーションは値の変更を少しずつ反映させるため、多分その時にtrue-falseの変更を繰り返しちゃっていますね。
また、falseにするとそれに対応してグラフが0になるようにアニメーションされてしまうため、上手くいきません。

### アニメーションする値を監視する
true-falseの管理だと上手くいきませんでした。
そこで、表示する種族値のバーの値を監視し、値が変更された時に変更された分だけアニメーションするようにします。

```StatsRowComponent.kt
@Composable
fun StatRowComponent(label: String, value: String) {
    var animatedValue by remember { mutableStateOf(0f) }

    LaunchedEffect(value) {
        // 画面幅に収まるようにサイズを0.8倍に調整
        animatedValue = (value.toFloat() * 0.8).toFloat()
    }

    val animation by animateFloatAsState(
        targetValue = animatedValue,
    )
        // 省略
        
        BaseStatsBarComponent(
            modifier = Modifier
                .width(animation.dp)
                .height(50.dp),
        )
        
        // 省略
}

```

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/4dd49824-c620-6920-a36d-81dd8431ce18.gif">


LaunchedEffectで受け取る種族値の値を監視し、値が変わった時にanimationValueが変更されることでアニメーションされるようにします。
こうすることで、次の問題へ進んだ時にもアニメーションしてくれるようになりました。

# Lottieを使い、さらに豪華にする
これで出題はアニメーションされるようになりましたが、もう一味加えたいです。

アニメーションを使う目的の1つに、「ユーザー体験のゴールを彩る」というものがあると勝手に思っています。
私の作った種族値クイズのアプリの場合、ユーザー体験のゴールは「クイズに答えて正解する」ことです。

特にこの種族値クイズは選択肢などがなく、割と難易度が高めに設定されているため、正解した時の演出ももう少し豪華にしたいと考えました。

そこで今回使うのが、Lottieというサービスです。

### Lottieとは
Lottieは、JSONベースのアニメーションファイル形式です。iOSやAndroidでも簡単に使うことができ、アニメーションファイルを作ったり見つけることができます。

https://lottiefiles.com/jp/

特にアニメーションがたくさんあり、「なんかお祝いしてる感じのアニメーション」をすぐに見つけることができました。
イメージと合って、良い感じ！

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/f23d5c23-294f-e324-745e-a91213ec3b2c.gif">

https://app.lottiefiles.com/animation/6b276faf-0367-4277-a403-72cfa298ad01?panel=download

### Lottieの導入
Littieを使うまでもすごく簡単です。
まずはimplementationを追加します。

```build.gradle.kts
dependencies {
    implementation "com.airbnb.android:lottie-compose:$lottieVersion"
}
```

そして、使いたいComposableの中で呼び出します。

```AnswerResultSection.kt
val composition by rememberLottieComposition(
        LottieCompositionSpec.RawRes(R.raw.animation_cerebration)
    )
    LottieAnimation(
        composition = composition,
    )

```

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524029/88fa020d-d76f-05ae-ad34-fda6a2e3d442.gif">


これだけで正解画面にお祝いのアニメーションを追加することができました！
あまりのお手軽さにびっくりしました。

# さいごに
ここまで読んでいただきありがとうございました。
無事アプリにアニメーションが追加できました。

アニメーションを追加したバージョンでぜひ遊んでみてください。感想などいただけますと嬉しいです！