JetpackCompose日記#4

## アニメーション

実際にアニメーションを実装する際には、以下の図を参考に決定する。(詳細は[こちら](https://developer.android.com/jetpack/compose/animation?hl=ja))
![img](https://developer.android.com/static/images/jetpack/compose/animation-flowchart.svg?hl=ja)

- [様々なアニメーション](https://developer.android.com/jetpack/compose/animation?hl=ja)

##### 簡単な値の変更をアニメーション化する

最初は Compose で最も簡単なアニメーション API の 1 つである ***animate*AsState API*** です。この API は、State の変更をアニメーション化する場合に使用します。

上部のホームボタンと仕事用ボタンをクリックしてタブを切り替えます。タブのコンテンツ自体は切り替わりませんが、コンテンツの背景色が変わります。

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/fe5b295b01d96139_1920.png?hl=ja" width=300 height="300" >
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/9c340d99866191b_1920.png?hl=ja" width=300 height=300>

変化する値を、対応する animate*AsState コンポーザブルのバリアント（この場合は animateColorAsState）でラップすることで、アニメーション値を作成できます。戻り値は State<T> オブジェクトであるため、[ローカルの委譲プロパティ](https://kotlinlang.org/docs/reference/delegated-properties.html#local-delegated-properties)と by 宣言を使用して、通常の変数のように扱うことができます。↓
```
@Composable
fun sample() {
    var tabPage by remember { mutableStateOf(TabPage.Home) }
×   val backgroundColor = if (tabPage == TabPage.Home) Purple100 else Green300
○   val backgroundColor by animateColorAsState(if (tabPage == TabPage.Home) Purple100 else Green300)
    ...
}
```

##### 表示をアニメーション化する
アプリのコンテンツをスクロールすると、フローティング アクション ボタンがスクロールの方向に応じて拡大および縮小されます。

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/2ee82acad1fcb370_1920.png?hl=ja" width="250" height="250">
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/f952a57647788799_1920.png?hl=ja" width="250" height="250">

この表示設定の変化をアニメーション化するには、if を AnimatedVisibility コンポーザブルに置き換えるだけです。↓

```
×　if (extended) {
○　AnimatedVisibility (visible = extended) {
   　 Text(
     　   text = stringResource(R.string.edit),
       　 modifier = Modifier
         　   .padding(start = 8.dp, top = 3.dp)
    　)
　}
```
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/7394e3cf71a3a9de.gif?hl=ja" width="300" height="300">

AnimatedVisibility は、指定された Boolean 値が変更されるたびにアニメーションを実行します。デフォルトでは、AnimatedVisibility は要素をフェードインして拡大することによって表示し、フェードアウトおよび縮小することによって非表示にします。この動作は、この例では FAB でうまく機能しますが、動作をカスタマイズすることもできます。

FAB をクリックすると、[Edit feature is not supported] というメッセージが表示されます。また、AnimatedVisibility を使用して表示と非表示をアニメーション化します。次に、メッセージを上からスライドイン、上にスライドアウトするようにこの動作をカスタマイズします。

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/40a612ce2997004_1920.png?hl=ja" width="350" height="300">

アニメーションをカスタマイズするには、AnimatedVisibility コンポーザブルに enter パラメータと exit パラメータを追加します。

enter パラメータは EnterTransition のインスタンスである必要があります。この例では、slideInVertically 関数を使用して、終了遷移の EnterTransition と slideOutVertically を作成します。次のようにコードを変更します。

```
@Composable
private fun EditMessage(shown: Boolean) {
    AnimatedVisibility(
        visible = shown,
        enter = slideInVertically(),
        exit = slideOutVertically()
)
...
}
```

開始遷移の場合、initialOffsetY パラメータを設定することで、アイテムの高さ全体を使用して適切にアニメーション化されるようにデフォルトの動作を調整できます。initialOffsetY は初期位置を返すラムダにする必要があります。

このラムダは 1 つの引数、つまり要素の高さを受け取ります。画面の最上部の値は 0 となっているため、アイテムを画面の最上部からスライドインさせるには、負の値を返します。アニメーションの開始位置を -height から 0（最後の静止位置）にし、上から動き始めるアニメーションになるようにします。

slideInVertically を使用する場合、スライドイン後のターゲット オフセットは常に 0（ピクセル）です。initialOffsetY は、絶対値として指定するか、ラムダ関数による要素の高さに対するパーセンテージとして指定できます。

同様に、slideOutVertically では初期オフセットが 0 に設定されているため、targetOffsetY のみを指定する必要があります。
```
...
        enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
        exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight })
...
```
animationSpec パラメータを使用すると、アニメーションをさらにカスタマイズできます。animationSpec は、EnterTransition や ExitTransition など、多くのアニメーション API で一般的なパラメータです。さまざまな AnimationSpec タイプのいずれかを渡して、***時間の経過にともなうアニメーション値の変化を指定***できます。この例では、シンプルな期間ベースの AnimationSpec を使用します。これは tween 関数を使用して作成できます。持続時間は 150 ミリ秒、イージングは LinearOutSlowInEasing です。終了アニメーションでは、animationSpec パラメータに同じ tween 関数を使用しますが、持続時間は 250 ms、イージングは FastOutLinearInEasing にします。

```
...
AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -> -fullHeight },
        animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -> -fullHeight },
        animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
    )
) 
...
```
これまでの結果が以下となる。
![image](https://developer.android.com/static/codelabs/jetpack-compose-animation/img/e8148c03e9364121.gif?hl=ja)

##### コンテンツサイズの変更をアニメーション化する
アプリでは、コンテンツに複数のトピックが表示されます。そのいずれかをクリックすると、そのトピックの本文テキストが表示されます。テキストを表示するカードは、本文を表示または非表示にするとそれに応じて拡大または縮小されます。

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/608ea85b76052d05_1920.png?hl=ja" width=250 height=250>
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/83c429a0cef946_1920.png?hl=ja" width=250 height=250>

Column コンポーザブルのサイズは、コンテンツの変更に応じて変わります。サイズの変化をアニメーション化するには、animateContentSize 修飾子を追加します。
```
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun TopicRow(topic: String, expanded: Boolean, onClick: () -> Unit) {
    TopicRowSpacer(visible = expanded)
    Surface(
        modifier = Modifier
            .fillMaxWidth(),
        elevation = 2.dp,
        onClick = onClick
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
  +             .animateContentSize()
        ) {
            Row {
                Icon(
                    imageVector = Icons.Default.Info,
                    contentDescription = null
                )
                Spacer(modifier = Modifier.width(16.dp))
                Text(
                    text = topic,
                    style = MaterialTheme.typography.body1
                )
            }
            if (expanded) {
                Spacer(modifier = Modifier.height(8.dp))
                Text(
                    text = stringResource(R.string.lorem_ipsum),
                    textAlign = TextAlign.Justify
                )
            }
        }
    }
    TopicRowSpacer(visible = expanded)
}
```
トピックのいずれかをクリックします。アニメーションにより拡大 / 縮小することがわかります。

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/c003d2a071d8630c.gif?hl=ja" width=250 height=250>

animateContentSize も、カスタムの animationSpec でカスタマイズできます。アニメーションのタイプを spring から tween などに変更できるようにすることができます。詳しくは、[アニメーションのカスタマイズ](https://developer.android.com/jetpack/compose/animation?hl=ja#customize-animations)に関するドキュメントをご覧ください。

##### 複数の値をアニメーション化する

より複雑なアニメーションを作成できる Transition API を見てみましょう。Transition API を使用すると、Transition のすべてのアニメーションが終了したタイミングをトラッキングできます。これは、前に説明した個々の animate*AsState API を使用する場合にはできないことです。また、Transition API を使用すると、状態遷移時に異なる transitionSpec を定義できます。

タブ インジケーターをカスタマイズします。これは、現在選択されているタブに表示される長方形です

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/fe5b295b01d96139_1920.png?hl=ja" width=250 height=250>
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/9c340d99866191b_1920.png?hl=ja" width=250 height=250>

タブインジケーターの実装は以下です
```
@Composable
private fun HomeTabIndicator(
    tabPositions: List<TabPosition>,
    tabPage: TabPage
) {
    // TODO 4: Animate these value changes.
    val indicatorLeft = tabPositions[tabPage.ordinal].left
    val indicatorRight = tabPositions[tabPage.ordinal].right
    val color = if (tabPage == TabPage.Home) Purple700 else Green800
    Box( ... 
```
ここで、indicatorLeft はタブ行のインジケーターの左端の水平位置です。indicatorRight はインジケーターの右端の水平位置です。色も紫と緑の間で変わります。

これらの複数の値を同時にアニメーション化するには、Transition を使用します。Transition は、updateTransition 関数を使用して作成できます。現在選択されているタブのインデックスを targetState パラメータとして渡します。

各アニメーション値は、Transition の animate* 拡張関数で宣言できます。この例では、animateDp と animateColor を使用します。それらはラムダブロックを使用します。また、各状態の目標値を指定できます。目標値はあらかじめわかっているため、次のように値をラップできます。なお、animate* 関数は State オブジェクトを返すため、ここでは by 宣言を使用して、ローカル委任プロパティにできます。

```
+ val transition = updateTransition(targetState = tabPage, label = "Tab indicator")
+   val indicatorLeft by transition.animateDp(label = "Indocator left") {page ->
        tabPositions[page.ordinal].left
    }
+   val indicatorRight by transition.animateDp(label = "Indicator right") { page ->
        tabPositions[page.ordinal].right
    }
+   val color by transition.animateColor(label = "Border color") { page ->
        if (page == TabPage.Home) Purple700 else Green800
    }
-    val indicatorLeft = tabPositions[tabPage.ordinal].left
-    val indicatorRight = tabPositions[tabPage.ordinal].right
-    val color = if (tabPage == TabPage.Home) Purple700 else Green800
```

アプリを実行すると、タブ切り替えがより面白いものになっています。タブをクリックすると tabPage 状態の値が変更されるため、transition に関連付けられたすべてのアニメーション値が、ターゲット状態に対して指定された値へのアニメーションを開始します。
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/452f7a5a34f2939f.gif?hl=ja" width=350 height=300>

さらに、transitionSpec パラメータを指定して、アニメーションの動作をカスタマイズできます。たとえば、目的地に近いエッジをもう一方のエッジよりも速く動かすことで、インジケーターの弾力性効果を実現できます。transitionSpec ラムダの isTransitioningTo 中置関数を使用して、状態変化の方向を決定できます。

```
val indicatorLeft by transition.animateDp(transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            spring(stiffness = Spring.StiffnessVeryLow)
        } else {
            spring(stiffness = Spring.StiffnessMedium)
        }
    }, ...
```
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/79376bb05022de00.gif?hl=ja" width=350 height=300>

##### アニメーションを繰り返す
読み込みが完了するまで、読み込みインジケーター（灰色の円と棒）が表示されます。処理が進行中であることを明確にするために、このインジケーターのアルファ値をアニメーション化します。

<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/cd37873f5a6102c4_1920.png?hl=ja" width=250 height=250>

```
val alpha = 1f
```
この値を 0f と 1f の間で繰り返しアニメーション表示します。このために InfiniteTransition を使用できます。この API は、前のセクションの Transition API に似ています。どちらも複数の値をアニメーション化しますが、Transition は状態変化に基づいて値をアニメーション化するのに対し、InfiniteTransition は値を無期限にアニメーション化します。

InfiniteTransition を作成するには、rememberInfiniteTransition 関数を使用します。その後、アニメーション化する各値の変化を、InfiniteTransition の animate* 拡張関数のいずれかで宣言できます。この場合、アルファ値をアニメーション化するため、animatedFloat を使用します。initialValue パラメータは 0f、targetValue パラメータは 1f である必要があります。このアニメーションには AnimationSpec も指定できますが、この API は InfiniteRepeatableSpec のみを使用します。infiniteRepeatable 関数を使用して作成します。この AnimationSpec は、持続時間ベースの AnimationSpec をラップし、繰り返せるようにします。たとえば、結果のコードは以下のようになります。
```
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
   initialValue = 0f,
  targetValue = 1f,
  animationSpec = infiniteRepeatable(
   animation = keyframes {
     durationMillis = 1000
     0.7f at 500
        },
   repeatMode = RepeatMode.Reverse
        )
    )
```
デフォルトの repeatMode は RepeatMode.Restart です。これは initialValue から targetValue に推移し、initialValue から再度始まります。repeatMode を RepeatMode.Reverse に設定すると、アニメーションは initialValue から targetValue に進み、さらに targetValue から initialValue に進みます。アニメーションは 0 から 1 になり、その後 1 から 0 に進みます。

keyFrames アニメーションは別の種類の animationSpec で（他には tween や spring があります）、ミリ秒単位の複数の時点で進行中の値を変化させることができます。最初に durationMillis を 1,000 ミリ秒に設定しました。次に、アニメーションでキーフレームを定義できます。たとえば、アニメーションの 500 ms で、アルファ値を 0.7f にします。これにより、アニメーションの進行が変化します。アニメーションの 500 ミリ秒の間に 0 から 0.7 に、アニメーションの 500 ミリ秒から 1,000 ミリ秒への間に 0.7 から 1.0 に迅速に進行し、終盤は遅くなります。

複数のキーフレームが必要な場合は、次のように複数の keyFrames を定義できます。
```
animation = keyframes {
   durationMillis = 1000
   0.7f at 500
   0.9f at 800
}
```
以下が結果↓
<img src="https://developer.android.com/static/codelabs/jetpack-compose-animation/img/58f4a9a93eabfbaf.gif?hl=ja" width=350 height=350>