0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Compose Animation 1.8で追加されたModifier.animateBoundsを使ってみる

Last updated at Posted at 2025-05-11

Compose1.8がリリースされました。TextのAutoSizeが追加されて話題となっていますが、本記事ではModifier.animateBounds()について紹介します。
レイアウト変更のアニメーションが簡単にできるようになり、アニメーションの幅が広がりました。

本記事では、Modifier.animateBounds()の基本概念と、実際に使用したサンプルコードを紹介します。

Modifier.animateBoundsとは

Modifier.animateBounds()はコンポーネントのサイズと位置の変化をアニメーションさせることができます。

これまではサイズの変化をアニメーションするだけならModifier.animateContentSize()を使えば良かったのですが、位置の変化のアニメーションは簡単にはできませんでした。
LookaheadScopeをつかて実装する例もありましたが、カスタムレイアウトで出てくるplaceableとかmesureableみたいなAPIを使う必要があり、サクッ作れる感じではなかったです。

しかしModifier.animateBounds()を使えば簡単にできるようになります。

下記例は画像の位置とサイズをアニメーションさせています。(画像にはAI生成画像を使っています。)

test4 (1).gif

LookaheadScope {
    var expanded by remember { mutableStateOf(false) }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Image(
            modifier = Modifier
                .offset(
                    x = if (expanded) 0.dp else 55.dp,
                    y = if (expanded) 0.dp else 50.dp
                )
                .size(if (expanded) 200.dp else 30.dp)
                .animateBounds(
                    this@LookaheadScope,
                    boundsTransform = BoundsTransform { _, _ ->
                        tween(2000)
                    }),
            painter = painterResource(id = R.drawable.shimaenaga),
            contentDescription = null
        )

        Button(onClick = { expanded = !expanded }) {
            Text(
                text = stringResource(
                    id = if (expanded) R.string.quiz_button_reset
                    else R.string.quiz_button_initial
                )
            )
        }

        if (expanded) {
            Text(text = stringResource(id = R.string.quiz_answer))
        }
    }
}

完全なコードはこちらです。

LookaheadScopeの解説

あまり聞き馴染みのない言葉だと思いますが、lookaheadとは、「先読み」みたいな意味です。
ドキュメントには以下のような説明があります。

[LookaheadScope] creates a scope in which all layouts will first determine their destination layout through a lookahead pass, followed by an approach pass to run the measurement and placement approach defined in [approachLayout] or [ApproachLayoutModifierNode], in order to gradually reach the destination.

要するに、LookaheadScopeはまず変化前と変化後のレイアウトを決定し、その間のパスを計算して、アニメーションを実現しているという理解です。

Modifier.animateBounds()は、LookaheadScopeの上に構築されています。

LookaheadScopeをそのまま使うのは上に書いたように少し難しさがあったのですが、Modifier.animateBounds()は簡単にレイアウト変更のアニメーションを実現できます。

movableContentOfまたはmovableContentWithReceiverOf<>と組み合わせていろいろなレイアウト変更をアニメーションする

Modifier.animateBounds()を使っていろいろなレイアウト間の変更をアニメーションすることができます。
しかしここで抑えておきたいのは、movableContentOfまたはmovableContentWithReceiverOf<>です。

上記APIが必要になってくるのは、Modifier.animateBounds()を使っている要素が、RowやColumnなどのレイアウトの中にある場合で、レイアウトが切り替わる場合です。

たとえばColumnを使ったレイアウトをRowのレイアウトに置き換える場合です。
普通に切り替えた場合、ColumnからRowに切り替わるとき、子の要素が再Compositionされてしまうので、うまくアニメーションできません。
そのため、movableContentOfまたはmovableContentWithReceiverOf<>を使うことで、子の要素を再Compositionせずに維持します。

(画像にはAI生成画像を使っています。)

test5.gif

LookaheadScope {
    var expanded by remember { mutableStateOf(false) }
    val toggleExpanded = { expanded = !expanded }
    val imageContent = remember {
        movableContentWithReceiverOf<LookaheadScope, Boolean> { expanded ->
            AsyncImage(
                model = R.drawable.camera,
                contentDescription = "カメラ画像",
                modifier = Modifier
                    .size(if (expanded) 400.dp else 200.dp)
                    .animateBounds(lookaheadScope = this)
                    .clickable {
                        toggleExpanded()
                    }
            )
        }
    }

    val cardContent = remember {
        movableContentWithReceiverOf<LookaheadScope> {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .animateBounds(lookaheadScope = this),
            ) {
                SpecText()
            }
        }
    }

    if (expanded) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            imageContent(true)
            cardContent()
        }
    } else {
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            imageContent(false)
            cardContent()
        }
    }
}

完全なコードはこちらです。

レイアウト変更のアニメーションがとても簡単にできるようになりました。
アプリのいろいろなUIで使い所がありそうですね。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?