🐻‍❄️

Jetpack Compose の Animated Content について深堀りする

2021/11/13に公開

はじめに

  • AnimatedVisibility では単一の View の Visibility を変化させるアニメーションをサポートしている。しかし新しい View を古い View に入れ替える際のアニメーションをサポートしていない。
  • AnimatedContent では主に新しい View から古い View に入れ替える際のアニメーションをサポートしていてクロスフェードしながら View を置き換えるアニメーションを実装できる。

値が変化したら View を置き換える

  • AnimatedContent の targetState には任意の型の値をセットできるようになっている
  • targetState に Int 型の count を設定すると count の変更に応じて View が再生成される
  • View が再生成されるときにはデフォルトで古い View がフェードアウト、新しい View がフェードインするアニメーションが設定されている。

counter1.gif

@ExperimentalAnimationApi
@Composable
fun AnimatedContentCounterDefault() {
    Column {
        var count: Int by remember { mutableStateOf(0) }

        AnimatedContent(targetState = count) { targetCount ->
            Text(
                text = "$targetCount",
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h2,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { count++ }) {
                Text("PLUS")
            }

            Button(onClick = { count-- }) {
                Text("MINUS")
            }
        }
    }
}

値が変化したらアニメーションしながら View を置き換える

  • View が再生成されるアニメーションは trasitionSpec にて変更できる。
  • transitionSpec には ContentTransform を返すラムダ関数を渡す必要がある。ちなみにContentTransformwith利用して以下の形式で作成できるようになっている。
ContentTransform の作り方
 - <EnterTransition> with <ExitTransition> という記述方法で作成する
 - <EnterTransition> には新しい View の遷移方法つまりアニメーション方法を指定する
 - <ExitTransition> には古い View の遷移方法つまりアニメーション方法を指定する

例1 数がインクリメントされたときに右から左に View が移動する ContentTransform を生成する
 - EnterTransition の slideInHorizontally の initialOffsetX には width を渡し右端から移動するアニメーションを開始するように
 - ExitTransition の slideOutHorizontally の targetOffsetX には -width を渡し左端に移動するアニメーションを開始するように
 
slideInHorizontally({ width -> width }) + fadeIn() with slideOutHorizontally({ width -> -width }) + fadeOut()

例2 数がデクリメントされたときに左から右に View が移動する ContentTransform を生成する 
 - EnterTransition の slideInHorizontally の initialOffsetX には -width を渡し左端から移動するアニメーションを開始するように
 - ExitTransition の slideOutHorizontally の targetOffsetX には width を渡し右端に移動するニメーションを開始するように

slideInHorizontally({ width -> -width }) + fadeIn() with slideOutHorizontally({ width -> width }) + fadeOut()
  • 今回は transitionSpec に数がインクリメントされたときには左から右に移動する、数がデクリメントされたときには右から左に移動するContentTransform を生成されるようにしてみた。そうすると次のようにインクリメントしたときには左から右にデクリメントしたときには右から左に数が移動するようになる。

counter2.gif

@ExperimentalAnimationApi
@Composable
fun AnimatedContentCounterCustom() {
    Column {
        var count: Int by remember { mutableStateOf(0) }

        AnimatedContent(
            targetState = count,
            transitionSpec = {
                val isPlus = targetState > initialState
                if (isPlus) {
                    slideInHorizontally({ width -> width }) + fadeIn() with slideOutHorizontally({ width -> -width }) + fadeOut()
                } else {
                    slideInHorizontally({ width -> -width }) + fadeIn() with slideOutHorizontally({ width -> width }) + fadeOut()
                }.using(
                    SizeTransform(clip = false)
                )
            }
        ) { targetCount ->
            Text(
                text = "$targetCount",
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h2,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { count++ }) {
                Text("PLUS")
            }

            Button(onClick = { count-- }) {
                Text("MINUS")
            }
        }
    }
}

値が変化したらサイズを変更しながら View を置き換える

  • transitionSpec で ContentTransform を作成する際にusing を利用して SizeTransform を加えアニメーション時に View のサイズを変更できる。
SizeTransform の加え方
 - <EnterTransition> with <ExitTransition> using <SizeTransform> という記述方式で ContentTransform を作成し using で SizeTransform を足す

SizeTransform の作り方
 - SizeTransform では keyframes を使ってどのようなアニメーションをするか指定できるようになっている
   - SizeTransform から渡される initialSize には開始時点での View の大きさ、targetSize には終了時点での View の大きさが渡される仕組みになっていて keyframes にてこの値を利用できる仕組みになっている。
 - keyframes ではどの大きさから、どこの時間から、どれくらいの時間をかけてアニメーションするか指定できるようになっている。SizeTransform から渡される initialSize と targetSize を利用してどのようなアニメーションするか決められる。

fadeIn() with fadeOut() using SizeTransform { initialSize, targetSize ->
	keyframes {
		// IntSize でどの大きさからアニメーションを開始するか指定できる、また at の後ろでどこの時間からアニメーションを開始するか指定できる
		IntSize(initialSize.width, initialSize.height) at 250

		// durationMillis どれくらい時間をかけてアニメーションをするか指定できる。
		durationMillis = 500
	}
}
  • 今回は View が縮まっているときにクリックすると View が広がる、View が広がっているときにクリックすると View が縮まるようにアニメーションを作成してみた。
  • 普通だとクリックに応じて View が切り替わるだけだったが SizeTransform を入れたことで View が切り替わるときに領域が広がりながら縮まりながら変化するようになる。

expand.gif

@ExperimentalMaterialApi
@ExperimentalAnimationApi
@Composable
fun AnimatedContentExpandableTextSample() {
    var expanded by remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.primary,
        onClick = { expanded = !expanded },
        modifier = Modifier.padding(8.dp)
    ) {
        AnimatedContent(
            targetState = expanded,
            transitionSpec = {
                fadeIn() with fadeOut() using SizeTransform { initialSize, targetSize ->
                    keyframes {
                        IntSize(initialSize.width, initialSize.height) at 250
                        durationMillis = 500
                    }
                }
            }
        ) { targetExpanded ->
            if (targetExpanded) {
                Text(
                    text = "Expanded",
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(100.dp)
                )
            } else {
                Text(
                    text = "Not Expanded",
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

おわりに

  • AnimatedContent を利用すれば View を置き換えたり切り替えたりするときの移動するアニメーションや拡大・縮小するアニメーションを実装できる。
  • AnimatedContent では with や using や at を利用してアニメーションを組み立てられるようになっている。しかしこれはなかなかわかりづらく正式リリースまでに調整が入りそうかなと感じた。

参考文献

Discussion