18
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Jetpack Composeアニメーション入門

Last updated at Posted at 2022-07-18

はじめに

composeの1.0.0安定版が出て一年と少し経ちましたが、
まだ本格的にcomposeを触れていないという方も多いのではないかと思います。

そこで、本記事ではAndroid開発の経験がある方向けに、
composeの主要な機能の一つであるアニメーションについて解説していきます。

なお、本記事では jetpack compose version: 1.2.0の情報を使用しています。

composeのアニメーションとは

Jetpackライブラリの一つであるJetpack Composeを構成するパッケージの一つです。
compose.animationとしてcomposeのuiなどとは別で提供されています。

パッケージ 内容
compose.animation アニメーションAPIを提供 (本記事で扱うパッケージ)
compose.material マテリアルデザイン2に対応したコンポーネントを提供
compose.material3 マテリアルデザイン3に対応したコンポーネントを提供(Material Youなど含む)
compose.foundation Scaffoldなどのui構築の基盤を提供
compose.ui 描画、入力などのuiの基本的なコンポーネントを提供
compose.runtime ComposeCompilerのコアランタイム
compose.compiler @Composable関数をkotlinコンパイラプラグインを使って変換する

composeは上図のように7つのパッケージから構成されています。
そして、compose.animationはその中の一つです。
composeのパッケージはそれぞれ依存関係が存在しますが、compose.animationはその中でも上位レイヤに位置するものです。

構成

composeのアニメーションAPIは、
簡単にuiにアニメーションを付できる高レベルアニメーションAPIと、
より柔軟にアニメーションをカスタム可能な低レベルアニメーションAPIの2つから構成されています。

高レベルアニメーションAPIはマテリアルデザインのアニメーションに合わせてカスタマイズされています。
また、低レベルアニメーションAPIを基盤に構築されており、低レベルアニメーションAPIのラッパーとも言えます。

特徴

開発元のgoogleはゼロから構築した新しいアニメーションAPIであると表現しています。
具体的に以下のような特徴があります

  • コルーチンベースのAPIであるため中断可能である
  • アニメーションを宣言的に記述できる
  • APIの多くがcomposableな関数として提供される
  • IDEのanimation previewでアニメーション実装をサポート
  • 高レベルアニメーションAPIを使用することで手軽にアニメーションを付与できる

特にコルーチンベースでアニメーションが作られていることが大きな特徴であり、とてもUIにアニメーションを組み込みやすくなっています。
具体的なユースケースとしては、ライフサイクルに合わせてアニメーションを自動的にキャンセルし、必要ならばアニメーションを自動的に再開するなどが挙げられます。

高レベルアニメーションAPI

ここからは、composeのアニメーションAPIからいくつかピックアップして、その使い方と特徴を紹介していきます。
まず、簡単にuiをアニメーションすることができる高レベルアニメーションAPIについて紹介します。

AnimatedVisibility

AnimatedVisibilityを使用することで、UIの表示、非表示切り替えを簡単にアニメーション化させることができます。
ezgif.com-gif-maker.gif

var isExtended by remember { mutableStateOf(false) }
FloatingActionButton(onClick = { isExtended = !isExtended }) {
  Row {
    // 表示非表示切り替えをアニメーション化する
    AnimatedVisibility(isExtended) {
      Text(text = "Add", Modifier.padding(start = 8.dp, end = 12.dp))
    }
    Icon(
      Icons.Default.Add,
      contentDescription = null,
      Modifier.padding(start = 8.dp end = 12.dp)
    )
  }
}

表示非表示を切り替える場合はif分で分岐させることが多いですが、その条件文をAnimatedVisibilityに変更するだけで使用できます。

- if(isExtended) {
+ AnimatedVisibility(isExtended) {
   Text(text = "Add", Modifier.padding(end = 12.dp, start = 8.dp))
   ...

デフォルトでは、アニメーションはフェードイン、フェードアウトですが、引数にTransitionを指定することでカスタムすることもできます。

AnimatedVisibility(isExtended, enter = scaleIn(), exit = scaleOut()) {
  ...

animateContentSize

animatedContentSize()はサイズの変更をアニメーション化することができます。

Card(
  modifier = Modifier
  .width(200.dp)
  .animateContentSize() // 子のサイズ変更に応じてサイズ変更をアニメーション化する
  .clickable {
    isExpanded = !isExpanded
  }
) {
  Column {
    AsyncImage(
      model = url,
      contentDescription = null
    )
    Text("title")
    if (isExpanded) {
      Text("content here.")
    }
  }
}

animatedContentSize()padding()などと同じようにmodifierの拡張関数として提供されています。
そのため非常に扱いやすいです。
よくあるcollapsingパネルなどを簡単にアニメーション化させることができます。

AnimatedContent

AnimatedContentは、上記二つのanimatedVisibility, animateContentSizeを合わせたようなアニメーションを作ることができます。
また、任意の状態の変化に応じてuiをアニメーション化させることができるため、他の高レベルアニメーションAPIよりも柔軟性が高いです。

gif

AnimatedContent(
  targetState = count, // countの変更に応じてアニメーション
  transitionSpec = { scaleIn() with scaleOut() }
) { targetState ->
  Box(
    Modifier
      .background(Color.White)
      .size(100.dp)
      .background(colors[targetState])
  ) {
    Text(
      modifier = Modifier.align(Alignment.Center),
      text = "count: $targetState"
    )
  }
}

アニメーションのターゲットとなる値をtargetStateとして引数に入れて使用します。
このtargetStateはラムダの引数で更新された値を取得できるのでuiではそれを使用して描画します。
また、transitionSpecの引数を使用することでアニメーションをカスタムすることも可能です。

拡張関数のwithを使用して開始時と終了時のアニメーションを指定できます。

transitionSpec = { enterTransition(開始時) with exitTransition(終了時) }

transitionはoperator funで定義されている演算子+を使用することで簡単に組み合わせることができます

transitionSpec = { scaleIn() + fadeIn() with scaleOut() + fadeOut() }

低レベルアニメーションAPI

次は低レベルアニメーションAPIについて紹介します。
上で紹介した高レベルアニメーションAPIは、これから説明する低レベルアニメーションAPIから構成されており、ただのラッパーになっているようなAPIも存在します。

animate*AsState

animate*AsStateは任意の単一の値の変化をアニメーション化させることができます。
*の部分にはAPIで定義されている型が入ります。ここで紹介する例では、Floatになっています。

val scale: Float by animateFloatAsState(if (isExpanded) 2f else 1f)
Box(
  Modifier
    .scale(scale)
    .background(Color.Magenta)
    .padding(32.dp)
)

animateFloatAsStateの引数にアニメーションさせたい値の開始時と終了時の値を入れれることで直感的にアニメーションを記述できます。
この例では、Boxのscaleを1倍 -> 2倍へと変更するアニメーションを作成しています。
アニメーションする値として他に提供されているものは、Color, Offset, Dpなどがあります。

animate*AsStateで作成した値をmodifierなどでそのまま装飾するだけで、アニメーション化させることが出来るので非常に使い勝手がいいです。

updateTransition

updateTransitionは任意の複数の値の変化を同時にアニメーション化させることができます。
animate*AsStateでは、単一の値の変化のアニメーションでしたが、updateTransitionは複数の値の変化をアニメーション化することができます。

val transition = updateTransition(targetState = isExpanded, label = "animation")
val scale: Float by transition.animateFloat(label = "scale") { isExpanded ->
  if (isExpanded) 2f else 1f
}
val color by transition.animateColor(label = "color") { isExpanded ->
  if (isExpanded) Color.Cyan else Color.Magenta
}

Box(
  Modifier
    .scale(scale)
    .background(color)
    .padding(32.dp)
)

実行中のアニメーションや状態を管理するTransitionを作成して、animate*関数を呼び出してアニメーション化させます。
この例では、先ほどのanimate*AsStateでサイズの値のみのアニメーションだったものを、updateTransitionを使ってサイズとカラーの2つの値のアニメーション化させています。

おそらく大抵のアニメーションはこのupdateTransitionを使用すれば実現できると思います。

Animatable

Animatableはこれまで紹介してきたAPIでは実現できない複雑な制御が必要とされるアニメーションを実装する場合に使用します。
Animatableを使うことで、アニメーションの速度と値をリアルタイムにトラックし、実行中のアニメーションがキャンセルされた時、新しいアニメーションをキャンセル時の状態から開始することができます。
ここでのキャンセルとは実行中のアニメーションが存在する場合に、新しいアニメーションを開始しようとすると実行中のアニメーションがキャンセルされるという意味です。

読んでもピンとこないと思いますので、実際にupdateTransitionAnimatableのスケールとカラーをアニメーション化した時のキャンセル時の違いを紹介します。

val animatedScale = remember { Animatable(1f) }
val animatedColor = remember { Animatable(Color.Magenta) }
LaunchedEffect(isExpanded) {
  // アニメーションの終了値
  val targetScale = if (isExpanded) 2f else 1f
  val targetColor = if (isExpanded) Color.Cyan else Color.Magenta
  // スケールとカラーの2つのアニメーションを同時に実行するために、launch関数で別々にコルーチンを起動する
  launch {
    animatedScale.animateTo(targetScale, animationSpec = tween(2000)) // tweenでduration:2秒間
  }
  launch {
    animatedColor.animateTo(targetColor, animationSpec = tween(2000))
  }
}

Box(
  Modifier
    .scale(animatedScale.value)
    .background(animatedColor.value)
    .size(120.dp)
)

// updateTransitionの方のコードは上で紹介したものとほぼ変わらないので省略

Animatableにアニメーション化する値を入れて、animateTo(ターゲットの値)を呼ぶことでアニメーションを実行します。
animateTo(ターゲットの値)関数はsuspend関数のためコルーチンスコープを用意する必要があります。
デフォルトで対応しているAnimatableに入れるアニメーション化する値は、Float, Colorだけですが、VectorConverterを使用することで他の値も使用できます。

キャンセル時について
アニメーションを実行中の状態で、ボタンをクリックするとアニメーションがキャンセルされて新しいターゲット値に向かってアニメーションされていることがgifからわかると思います。ここで、updateTransitionAnimatableで動作に違いが存在します。

  • updateTransitionでは、アニメーションのターゲット値が変更されると、実行中のアニメーションはキャンセルされずにそのまま変更されたターゲット値に向かうようにアニメーションが自動で調整されます。そのためtween(2000)の動きを無視して新しいターゲット値に向かってアニメーションが自動で実行されています。
  • Animatableでは、実行中のアニメーションが存在する場合に新しくanimateToでアニメーションを開始すると、コルーチンのCancellationExceptionがスローされ実行中のアニメーションがキャンセルされます。そのキャンセル時のアニメーションの速度や状態をAnimatableが保持しており、新しいアニメーションはその値を開始時のアニメーション状態として、新しいターゲット値に向かってアニメーションが実行されます。

このようにAnimatableを使用することで、キャンセル時の制御を細かく行うことができ、アニメーションのターゲット値を柔軟に変更しuiをそれに追従させることができます。

このAPIは少し難しいので公式ドキュメントで紹介されていた使い方も載せておきます。
コード: https://developer.android.com/reference/kotlin/androidx/compose/animation/core/Animatable

タップした位置にoffsetでboxを移動させるアニメーションです。
移動中に新しいタップ入力が来たときにそれをキャンセルし、新しいタップ位置にアニメーションさせています

composeで実現できないこと

composeのアニメーションの魅力だけ伝えてきましたが、現状まだ実現できないアニメーションも存在しています。
具体的には以下のアニメーションなどが挙げられます

  • MotionLayout
  • Shared Element Transition
  • Navigation Animation (accompanistに入っているが少し不安定なのと、compose.animationに統合され削除される予定で提供されている)

ただ、これらの未対応アニメーションはgoogleが今後対応していきたいと言っているので、対応されるのも時間の問題と思われます。

おまけ

最後にcomposeのアニメーションで遊びで作ったものを紹介します

  • rememberInfiniteTransition + グラデーション
    簡単にshimmer loadingを作ってみました。
    rememberInfiniteTransitionは使い方的にはupdateTransitionに似ている低レベルアニメーションAPIです。
    無限に繰り返すアニメーションを実装することができます。
  • Animatable + Canvas
    Animatableを使ってウマ娘のアニメーションを作ってみました(kawaii)
    composeでもcanvasが使用できるので柔軟に作りたい形をドローイングすることができます。

素材: https://www.youtube.com/watch?v=DE0rfFfd8fY

参考

公式ドキュメント: https://developer.android.com/jetpack/compose/animation
コードラボ: https://developer.android.com/codelabs/jetpack-compose-animation

18
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?