Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Jetpack Composeを使った複雑なアニメーション事例

ミラティブのAndroidエンジニアのmorizoooです。

MirrativのAndroidアプリでは、新規で作る画面には積極的にJetpack Composeを活用しています。

tech.mirrativ.stream

Mirrativは、多くのユーザーがゲームや雑談などの配信をしており、配信を盛り上げるための演出として通常のアニメーションに加えてLottieも織り交ぜ、リッチなアニメーションを実装しています。

Jetpack Compose導入以前は、AnimationListenerのCallbackを入れ子にする形で複雑なアニメーションを実現していたのですが、いざJetpack Composeで実装しようとなるとどう実装するのが良いのか苦心しました。

今回の記事では、実際にアプリで使っているアニメーションを例に、Jetpack Composeで作ったアニメーションを紹介します。

Animatableを使ったアニメーション処理

Jetpack Composeのアニメーションについては、Googleの公式の記事がものすごくわかりやすいです。

developer.android.com

MirrativのAndroidでは、Jetpack Composeでアニメーションを順次処理するためにAnimatableのsuspend functionであるanimateToを使うケースが多いです。 例えば以下のようにテキストを3つ順にフェードで出していくアニメーションをAnimatableで実現するには以下のようになります。

@Composable
fun AnimatableSample() {
    val text1AlphaAnimatable = remember { Animatable(0f) }
    val text2AlphaAnimatable = remember { Animatable(0f) }
    val text3AlphaAnimatable = remember { Animatable(0f) }

    LaunchedEffect(true) {
        text1AlphaAnimatable.animateTo(
            targetValue = 1f,
            animationSpec = tween(durationMillis = 500)
        )
        delay(500)
        text2AlphaAnimatable.animateTo(
            targetValue = 1f, 
            animationSpec = tween(durationMillis = 500)
        )
        delay(500)
        text3AlphaAnimatable.animateTo(
            targetValue = 1f, 
            animationSpec = tween(durationMillis = 500)
        )
    }

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        contentAlignment = Alignment.Center
    ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(
                modifier = Modifier.alpha(text1AlphaAnimatable.value),
                text = "テキスト1"
            )
            Spacer(modifier = Modifier.height(20.dp))
            Text(
                modifier = Modifier.alpha(text2AlphaAnimatable.value),
                text = "テキスト2"
            )
            Spacer(modifier = Modifier.height(20.dp))
            Text(
                modifier = Modifier.alpha(text3AlphaAnimatable.value),
                text = "テキスト3"
            )
        }
    }
}

Lottie Animatableを組み合わせたアニメーション処理

Lottieは、AdobeのAfter Effectsで出力したjsonファイルを解析し、ネイティブでアニメーションをレンダリングするためのライブラリです。 LottieはJetpack Composeもサポートしていて、公式の解説もあります。

github.com

LottieにもLottie AnimatableというAnimatableのようなclassがあり、Lottie Animatableのsuspend functionのanimateを使って順次処理しています。 例えば以下のようなアニメーションを処理する場合には、AnimatableとLottie Animatableを組み合わせて実現しています。

  1. View全体を背景半透明の黒でフェードイン(Animatable)
  2. 宝箱を表示+開封(Animatable + Lottie Animatable)
  3. 2.のアニメーション開始から3000milliSec後に入手したアイテムをフェードイン

Animatable + Lottie Animatableで実現したコード

@Composable
fun LottieAnimatableSample() {
    // 1. View全体を背景透過の黒でFadeInするためのAnimatable
    val containerAlphaAnimatable = remember { Animatable(0f) }

     // 2. 宝箱を表示するための開封Animatable + Lottie Animatable
    var isVisibleOpenTreasureAnimation by remember { mutableStateOf(false) }
    val openTreasureLottieComposition by rememberLottieComposition(LottieCompositionSpec.Asset(assetName = "open_tresure.json"))
    val openTreasureLottieAnimatable = rememberLottieAnimatable()

    // 3. 2.のアニメーション開始から3000milliSec後に入手したアイテムをFadeInするためのAnimatable
    val bonusItemAlphaAnimatable = remember { Animatable(0f) }

    LaunchedEffect(true) {
        containerAlphaAnimatable.animateTo(
            targetValue = 1f,
            animationSpec = tween(durationMillis = 500)
        )
        launch {
            isVisibleOpenTreasureAnimation = true
            openTreasureLottieAnimatable.animate(composition = openTreasureLottieComposition)
        }
        delay(3000)
        bonusItemAlphaAnimatable.animateTo(
            targetValue = 1f,
            animationSpec = tween(durationMillis = 500)
        )
    }

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
            .alpha(containerAlphaAnimatable.value)
            .background(Color(0xCC000000)),
        contentAlignment = Alignment.Center,
    ) {
        if (isVisibleOpenTreasureAnimation) {
            LottieAnimation(
                composition = openTreasureLottieComposition,
                progress = openTreasureLottieAnimatable.progress,
            )
        }
        // MirrativImage: httpsの画像を読み込むImageクラス
        MirrativImage(
            imageUrl = "入手したアイテムのURL",
            Modifier
                .alpha(bonusItemAlphaAnimatable.value)
                .width(128.dp)
                .height(128.dp)
        )
    }
}

FYI: AnimationListenerを使ったコード

参考までにJetpack Compose以前のAnimationListenerを使ったコードのサンプルを記載します。(※レイアウトのxmlは省略)

binding.root.doOnLayout {
    binding.rootContainer.startAnimation(
        AlphaAnimation(0f, 1f).apply {
            duration = 500
            setAnimationListener(object : Animation.AnimationListener {
                override fun onAnimationEnd(animation: Animation) {
                    binding.openTresureLottieAnimationView.visibility = View.VISIBLE
                    launch {
                        delay(3000)
                        binding.bonusItemImageView.visibility = View.VISIBLE
                        binding.bonusItemImageView.startAnimation(AlphaAnimation(0f, 1f).apply { duration = 500 })
                    }
                }

                override fun onAnimationEnd(animation: Animation) {
                }

                override fun onAnimationRepeat(animation: Animation) {
                }
            })
        }
    )
}

今回は紹介のため、一部分のみ簡易な形で紹介しましたが、実際はもう少し複雑なアニメーションを作っています。 以下は冒頭のアニメーションと同じものになりますが、エンジニアがアニメーションを実装するためにデザイナーがFigmaで作ってくれた仕様書になります。

アニメーションの仕様書

まとめ

複雑なアニメーションをJetpack Composeで実現した事例について紹介しました。 Mirrativでは、新規画面ではJetpack Composeでの実装を必須しています。

その中でリッチなアニメーションを実装することが多く、早めにJetpack Composeでのアニメーションの実装方針を作る必要がありました。 今回の実装がベストかどうかはわかりませんが、他社での事例があまりないことだったので、今回記事にしてみました。

少しでも参考になった方が居ますと幸いです。こうした方が良いよ、などあればコメントくださると嬉しいです!

We are hiring!

ミラティブでは一緒にアプリを作ってくれる Android エンジニアを募集中です!気軽にご連絡ください! www.mirrativ.co.jp

speakerdeck.com