Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Android Jetpack Compose Animation ~強調表示アニメーションのTextを作ってみる~

こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。

今回は Jetpack Compose のアニメーションを用いて、強調表示アニメーションの Text を作っていきたいと思います。

今回作成したいもの

まず本記事のゴールについてです。下記のように文字が順番に強調表示されていく Text を作成してみたいと思います。

アニメーションについて

Jetpack Compose のアニメーションは下記の公式 developers サイトに載っています。今回はこの中の AnimatedVisibility を用いて、強調表示アニメーションを実現していきたいと思います。

developer.android.com

実装

今回の実装は完成までを 3 段階構成にし、1 つずつ行っていきます。

  • 【第 1 段階】Text をアニメーションで fade in させる
  • 【第 2 段階】2 つの Text を重ねて、色が変わったように見せる
  • 【第 3 段階】1 文字ずつ Text を作成し、順番にアニメーションを実行する


【第 1 段階】Text をアニメーションで fade in させる

まず Text で AnimatedVisibility の fade in アニメーションを行うようにします。


■AnimatedVisibility

アニメーションしたい Text を AnimatedVisibility で囲います。そして AnimatedVisibility の visible に Boolean の State を設定します。この Boolean を true にすることで、 fade in のアニメーションが実行されます。


■LaunchedEffect

今回は「一定時間を置いて、文字が表示される」ようなアニメーションがしたいため、Coroutines を用いて遅延してアニメーションを実行したいと思います。Compose で Coroutines を使用するには、 LaunchedEffect を使用します。

developer.android.com

LaunchedEffect は Composable がコンポジションされた時に実行されます。この中で delay を実行することで指定した時間待機することができ、その後 visible を true にすることでアニメーションを実行することができます。


■実装

@Composable
fun AnimationText(
        text: String,
        〜〜省略〜〜
        durationMillis: Long
) {
    var visible by remember { mutableStateOf(false) }
    AnimatedVisibility(
            visible = visible,
            enter = fadeIn(initialAlpha = 1f)
    ) {
        Text(
                text = text,
                〜〜省略〜〜
        )
    }
    LaunchedEffect(Unit) {
        delay(durationMillis)
        visible = true
    }
}


■結果

上記実装により時間差で Text を fade in アニメーションすることができるようになりました。



【第 2 段階】2 つの Text を重ねて、色が変わったように見せる

続けてどのように Text の文字を変化させるかを対応していきます。今回は同じ文字の Text を 2 つ重ねて、片方の文字の visible を false から true に変えることで強調表示になったように見せていきたいと思います。

実装としては下記になります。 Box を用いて文字を重複させ、変化前の Text を下に、変化後の Text を上に重なるように設置します。そして変化後の Text を【第 1 段階】で作成した fade in アニメーションで表示させるようにすれば完成です。


■実装

@Composable
fun AnimationText(
        text: String,
        color: Color,
        fontSize: TextUnit,
        fontWeight: FontWeight? = null,
        〜〜省略〜〜
        effectColor: Color,
        effectFontWeight: FontWeight?,
        durationMillis: Long
) {
    var visible by remember { mutableStateOf(false) }
    Box {
        Text(
                text = text,
                modifier = modifier,
                color = color,
                fontSize = fontSize,
                fontWeight = fontWeight,
                〜〜省略〜〜
        )
        AnimatedVisibility(
                visible = visible,
                enter = fadeIn(initialAlpha = 1f)
        ) {
            Text(
                    text = text,
                    color = effectColor,
                    fontSize = fontSize,
                    fontWeight = effectFontWeight,
                    〜〜省略〜〜
            )
        }
    }
    LaunchedEffect(Unit) {
        delay(durationMillis)
        visible = true
    }
}


■結果

これで文字全てを強調表示に変化するようになりました。



【第 3 段階】1 文字ずつ Text を作成し、順番にアニメーションを実行する

最後にこのアニメーションを 1 文字ずつに設定して、順番に実行していきたいと思います。【第 2 段階】までで作成した Text の Compose を 1 文字ずつ作成し、水平方向に並べることで実現していきます。

■LazyVerticalGrid

通常 Compose で水平方向に並べる場合は Row を使用しますが、今回は LazyVerticalGrid を用います。これは Grid 表示で水平方向に並べつつ、入りきらない場合は次の行に移動してくれます。これを利用して文字数が多い場合に、通常の Text のように次の行に改行して表示できるようになります。

LazyVerticalGrid の引数に columns があります。これは 1 要素のサイズになるため、今回は fontSize を指定します。指定は dp 指定となるため、fontSize を sp 指定にしている場合は下記のように変換する必要があります。

また、LazyVerticalGrid の要素の中で items の API を使用することができます。 これで LazyVerticalGrid の 1 要素を作成していきます。先ほどの 【第 2 段階】までで作成した Text をこの中で作成するように実装します。

LazyVerticalGrid(
         columns = GridCells.Adaptive(
                  minSize = with (LocalDensity.current) {
                           fontSize.toDp()
                  }
         ),
         modifier = modifier
) {
         items(text.length) { index ->
                  ~~1文字ずつ作成~~
         }
}


■MutableState

ここまではアニメーションの切り替えを Boolean 値で対応をしていましたが、文字を順番にアニメーションする関係により MutableState を用いて表示状態を持つ必要があります。index を key とした Map を変数として作成し、文字 1 文字を作成する段階で表示状態の MutableState を visible に設定し、その状態を Map の value に格納するようにします。そしてアニメーションはその Map を最初から一定時間で更新していくことで、文字が順番にアニメーションするようになります。

 private val visibleMap = mutableMapOf<Int, MutableState<Boolean>>()
 
 ~~省略~~
 LazyVerticalGrid(
            〜〜省略〜〜
) {
    items(text.length) { index ->
        val textItem: String = text[index].toString()
        val visible = remember { mutableStateOf(false) }
        Box {
            〜〜省略〜〜
           AnimatedVisibility(
                        visible = visible.value,
                        enter = fadeIn(initialAlpha = 1f)
            ) {
            〜〜省略〜〜
            }
        }
        visibleMap[index] = visible
    }
}
LaunchedEffect(Unit) {
    visibleMap.forEach { (_, visible) ->
            delay(durationMillis)
            visible.value = true
        }
    }
}

以上で実装は完了です。

最終的な実装は下記になりました。

■実装

private val visibleMap = mutableMapOf<Int, MutableState<Boolean>>()
@Composable
fun AnimationText(
        text: String,
        modifier: Modifier = Modifier,
        color: Color,
        fontSize: TextUnit,
        fontStyle: FontStyle? = null,
        fontWeight: FontWeight? = null,
        fontFamily: FontFamily? = null,
        effectColor: Color,
        effectFontWeight: FontWeight? = null,
        durationMillis: Long = 500
) {
    LazyVerticalGrid(
            columns = GridCells.Adaptive(
                    minSize = with (LocalDensity.current) {
                        fontSize.toDp()
                    }
            ),
            modifier = modifier
    ) {
        items(text.length) { index ->
            val textItem: String = text[index].toString()
            val visible = remember { mutableStateOf(false) }
            Box {
                Text(
                        text = textItem,
                        modifier = modifier,
                        color = color,
                        fontSize = fontSize,
                        fontStyle = fontStyle,
                        fontWeight = fontWeight,
                        fontFamily = fontFamily
                )
                AnimatedVisibility(
                        visible = visible.value,
                        enter = fadeIn(initialAlpha = 1f)
                ) {
                    Text(
                            text = textItem,
                            modifier = modifier,
                            color = effectColor,
                            fontSize = fontSize,
                            fontStyle = fontStyle,
                            fontWeight = effectFontWeight,
                            fontFamily = fontFamily
                    )
                }
            }
            visibleMap[index] = visible
        }
    }
    LaunchedEffect(Unit) {
        visibleMap.forEach { (_, visible) ->
            delay(durationMillis)
            visible.value = true
        }
    }
}


■結果

これで 1 文字ずつの強調表示アニメーションができるようになりました。



まとめ

Jetpack Compose のアニメーション AnimatedVisibility の fade in アニメーションを用いて、文字が順番に強調表示されていく Text を作成してみました。xml の時ではちょっと手が止まってしまいそうなアニメーションも Compose であれば動的に分かりやすい実装ができるなとあらためて感じました。応用が効くため、これからも色々なパターンで挑戦してみたいと思います。