Jetpack Composeアニメーション入門

# はじめに
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つから構成されています。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/4253150e-7d97-98f1-ec82-7730e8e9bffd.png"/>

`高レベルアニメーション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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/c24aee3e-e246-5e09-6edb-06ef672b2c3e.gif)

```kotlin
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`に変更するだけで使用できます。

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

デフォルトでは、アニメーションはフェードイン、フェードアウトですが、引数にTransitionを指定することでカスタムすることもできます。
```kotlin
AnimatedVisibility(isExtended, enter = scaleIn(), exit = scaleOut()) {
  ...
```


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

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/ca7416c6-c6a1-5aef-c7a6-74874d316eb2.gif"/>


```kotlin
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/5961ef65-af6a-4d1b-9291-bbd00638be15.gif)


```kotlin
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`を使用して開始時と終了時のアニメーションを指定できます。
```javascript
transitionSpec = { enterTransition(開始時) with exitTransition(終了時) }
```

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

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


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

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

<img width=250 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/21c0d329-f833-8233-a020-59af751ee47e.gif"/>


```kotlin
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`は複数の値の変化をアニメーション化することができます。

<img width=250 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/68f51bf7-eb86-c943-1315-873bb5b5e954.gif"/>

```kotlin
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`を使うことで、アニメーションの速度と値をリアルタイムにトラックし、**実行中のアニメーションがキャンセルされた時、新しいアニメーションをキャンセル時の状態から開始**することができます。
ここでのキャンセルとは実行中のアニメーションが存在する場合に、新しいアニメーションを開始しようとすると実行中のアニメーションがキャンセルされるという意味です。

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

<img width=180 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/b7e653cc-8360-d7e6-3f1a-c6b9d2766c19.gif"/>

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

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

このAPIは少し難しいので公式ドキュメントで紹介されていた使い方も載せておきます。
コード: https://developer.android.com/reference/kotlin/androidx/compose/animation/core/Animatable
<img width=250 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/e2bbe23d-9a31-0b33-3ca1-487c0616064f.gif"/>
タップした位置にoffsetでboxを移動させるアニメーションです。
移動中に新しいタップ入力が来たときにそれをキャンセルし、新しいタップ位置にアニメーションさせています


# composeで実現できないこと
composeのアニメーションの魅力だけ伝えてきましたが、現状まだ実現できないアニメーションも存在しています。
具体的には以下のアニメーションなどが挙げられます
- **MotionLayout**
- **Shared Element Transition**
- **Navigation Animation** (accompanistに入っているが少し不安定なのと、compose.animationに統合され削除される予定で提供されている)

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

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

- `rememberInfiniteTransition` + グラデーション
簡単にshimmer loadingを作ってみました。
`rememberInfiniteTransition`は使い方的には`updateTransition`に似ている低レベルアニメーションAPIです。
無限に繰り返すアニメーションを実装することができます。

<img width=450 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/ba9c65bd-4702-718f-8bc1-8e27cbb51832.gif"/>

- `Animatable` + `Canvas`
`Animatable`を使ってウマ娘のアニメーションを作ってみました(kawaii)
composeでもcanvasが使用できるので柔軟に作りたい形をドローイングすることができます。
<img width=550 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/a3075764-bd55-ce27-5c22-651db999a8fd.gif"/>

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

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