Material design 3 のContainerTransitionをComposeで再現する

# Container Transition
https://m3.material.io/styles/motion/transitions/transition-patterns#b67cba74-6240-4663-a423-d537b6d21187

![ezgif-5-7fbe0f647e.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/c8d6fb35-10de-d05b-f943-2ddddc691ded.gif)

記憶違いでなければdesign 2 の頃からあったこのアニメーション。ComposeがPlannedのステータスのまま、今年のIOでも（モーション拘るっていう趣旨の発言がありつつも）特に更新がなかったやつです。

# 作ってみた

![animate_container.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/b9f4f711-5b24-946d-bf15-594b38b0f726.gif)

# 結論

細かいところはあまり考えてないです。
transition後のスクリーンの要件次第ですが、意外と基本的なアニメーション部品の組み合わせでできちゃってます。 が公式待った方がよいかなって所感です。

やろうと思えば汎用的にできそうですが、必要になったらやります。（ちゃんと探せばいいライブラリとかありそう。。）

コードは以下。

https://github.com/kkhouse/Playground/tree/main/AndroidProject/animation/AnimatedContentPlayGround

# ざっくり解説

## アプローチ
Rowのなかの1個のComposableから、どうやって画面全体に広げるか考えたところ以下が浮かびました。

1. タップされたComposableの座標(offset)とサイズ（)を取得
2. 1をつかって、同じ箇所に同じLayoutのComposableをまず作る
3. AnimatedVisibilityとTransitionでそいつをひろげてく

単純に画像だけ広げてみたのが以下です。

![animate_container2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/a1623b9c-68ef-cc29-f576-2e526aaf1d3e.gif)

コード（抜粋
```kotlin
data class TargetViewInfo(
    val topLeft: DpOffset = DpOffset(0.dp,0.dp),
    val width: Dp = 0.dp,
    val height: Dp = 0.dp,
    val data: SampleData = SampleData()
)
...
// Image or Box
.onGloballyPositioned {
    with(density) {
        targetViewInfo = targetViewInfo.copy(
            topLeft = DpOffset(
                // TODO Fix : Rootからだと、親Composeの影響を受けそう
                x = it.positionInRoot().x.toDp(),
                y = it.positionInRoot().y.toDp()
            ),
            width = it.size.width.toDp(),
            height = it.size.height.toDp()
        )
    }
}
```
onGloballyPositionedはサクッとComposableの情報取ってくるのに便利。リコンポーズが走るので多用はNGなきがしますが、、
とりあえず上記で1 をやってます。


```kotlin
@Composable
fun SampleContainerAnimationBox(
    data : TargetViewInfo,
    isVisible: Boolean,
    onBackPressed: () -> Unit = {},
    content: @Composable BoxScope.() -> Unit = {},
    transitionDuration: Int = 500
) {
    val screenWidth = LocalConfiguration.current.screenWidthDp
    val screenHeight = LocalConfiguration.current.screenHeightDp
    AnimatedVisibility(
        visible = isVisible,
        enter = EnterTransition.None,
        exit = ExitTransition.None
    ) {
        /*
        各アニメーション値をAnimatedVisibilityScopeのtransitionで定義することで
        animatedVisibilityの表示非表示のタイミングをアニメーション値の生存期間と一致させる
         */
        val width by transition.animateDp(
            transitionSpec = { tween(durationMillis = transitionDuration) },
            label = "content width"
        ) { state -> if (state == EnterExitState.Visible) screenWidth.dp else data.width }
        val height by transition.animateDp(
            transitionSpec = { tween(durationMillis = transitionDuration) },
            label = "content height"
        ) { state -> if (state == EnterExitState.Visible) screenHeight.dp else data.height }
        val offsetX by transition.animateDp(
            transitionSpec = { tween(durationMillis = transitionDuration) },
            label = "content offset x"
        ) { state -> if (state == EnterExitState.Visible) 0.dp else data.topLeft.x }
        val offsetY by transition.animateDp(
            transitionSpec = { tween(durationMillis = transitionDuration) },
            label = "content offset y"
        ) { state -> if (state == EnterExitState.Visible) 0.dp else data.topLeft.y }
        Box(
            modifier = Modifier
                .size(width = width, height = height)
                .offset(offsetX, offsetY)
                .background(Color.White)
        ) {
            ImageContainerScreen(
                res = data.data.img,
                transitionDuration = transitionDuration
            )
        }
    }

    BackHandler {
        onBackPressed()
    }
}
```

あとは取得したサイズや座標をアニメーションでラップしてBoxにそれぞれ指定するだけです。 2,3 。

AnimatedVisibilityScope.transitionからアニメーション値を設定してAnimatedVisibility自体にはNoneを設定してます。
これは元画面に戻る時にComposableをアニメーションしつつ消すって時に便利で、こうしておくとAnimatedVisivility配下で設定したtransition.animate*asStateのアニメーションが全て終了した時にComposableが削除されるように動いてくれるので、よけいな表示非表示の状態管理を考えなくて済むのがよところです。

https://developer.android.com/jetpack/compose/animation?hl=ja#animatedvisibility-add-custom-animation


`ImageContainerScreen`
この中身はいくらでも変えられるので、そこそこ柔軟な部品になりそうではある。