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?

Modifier.animateBoundsを使って実用的なレイアウト変更をアニメーションする

Posted at

Jetpack Compose 1.8 で追加されたModifier.animateBoundsは、レイアウト変更を滑らかにアニメーションできる便利な API です。
前回の記事(Compose Animation 1.8 で追加された Modifier.animateBounds を使ってみる)では、基本的な使い方を紹介しましたが、ただ使ってみたというだけの、どこで使うUIなんだというものでした。今回はより実践的な UI 例と、実装時の工夫・注意点をまとめます。
なお、前回同様、画像にはAI生成画像を使っています。

目次

実践的な UI 実装例

例えば、アプリでよく見かける「リスト表示とグリッド表示の切り替え」をアニメーション付きで実現したい場合、Modifier.animateBoundsが非常に役立ちます。
アニメーションはありませんが、DailyArtというアプリでこのUIを見かけたので、興味があればご覧ください。

  • List 表示時は画像+テキスト、Grid 表示時は画像のみを表示
  • レイアウト切り替え時に、画像やテキストの位置・サイズが滑らかに変化

実際の動きは以下の GIF で確認できます。

test6.gif

サンプルコード

完全なサンプルはこちらにあります。
以下は、Grid/List 切り替え部分の抜粋です(切り替えボタン等は省略しています)。

var isGrid by remember { mutableStateOf(true) }

val items = remember {
    movableContentWithReceiverOf<LookaheadScope, Boolean> { isGrid ->
        val imageWidth = (LocalConfiguration.current.screenWidthDp - 48) / 2
        birds.forEach { bird ->
            Row(
                modifier = Modifier
                    .animateBounds(lookaheadScope = this@movableContentWithReceiverOf)
            ) {
                AsyncImage(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(bird.imageResId)
                        .build(),
                    contentDescription = bird.name,
                    modifier = Modifier
                        .size(if (isGrid) imageWidth.dp else 128.dp)
                        .animateBounds(lookaheadScope = this@movableContentWithReceiverOf)
                        .clip(RoundedCornerShape(8.dp))
                )
                AnimatedVisibility(
                    !isGrid,
                ) {
                    Column(
                        modifier = Modifier
                            .padding(8.dp)
                            .animateBounds(lookaheadScope = this@movableContentWithReceiverOf),
                    ) {
                        Text(
                            text = bird.id.toString(),
                            style = MaterialTheme.typography.bodyMedium,
                            modifier = Modifier.padding(start = 8.dp),
                        )
                        Text(
                            text = bird.name,
                            style = MaterialTheme.typography.titleMedium,
                            modifier = Modifier.padding(start = 8.dp),
                        )
                    }
                }
            }
        }
    }
}

LookaheadScope {
    if (isGrid) {
        FlowRow(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(top = 16.dp, start = 16.dp),
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp),
            maxItemsInEachRow = 2
        ) {
            items(isGrid)
        }
    } else {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            items(isGrid)
        }
    }
}
  • LookaheadScope内でModifier.animateBoundsを使うことで、レイアウト変更時のアニメーションが可能になります。
  • 子要素の再構成を避けるためにmovableContentWithReceiverOfを利用しています。
  • テキスト部分の表示/非表示はAnimatedVisibilityで切り替えています。

movableContentWithReceiverOfでは、その中でisGridのフラグを受け取れるようにジェネリクスを指定しています。
もう少しコードに改善の余地があるかもですが、とりあえずこれで違和感なく動きます。

Modifier.animateBoundsの概要

さて、実装上の注意に入る前に簡単にModifier.animateBoundsの概要を説明します。
本格的なしくみの説明は別の記事を書こうかなと思っています。

Modifier.animateBoundsを使うにはLookaheadScopeが必要でした。
LookaheadScopeは、レイアウトの変更を事前に検知し、アニメーションを適用するためのスコープです。

lookaheadとは、先読み、の意味です。これから起こり得るレイアウト変更を先読みして、animateBoundsがついたコンポーネントのサイズや位置を徐々にアニメーションで変化させる、という理解です。

image.png

Modifier.animateBoundsをつけると、その直接の子にレイアウト変更のアニメーションが適用されます。
またModifier.animateBoundsをつけたコンポーネントに、別のModifierチェーンでサイズ変更などをしてたりすると、その変化もアニメーションされます。
その場合適用順番は大事で、animateBoundsをつけた要素にはその前につけていたModifierチェーンの変更しかアニメーションが適用されなかったり、親のコンポーネントの同じ階層にあるコンポーネントへの影響があったりするので、注意が必要です。

(適用順に関してはこちらの公式ページに記載がありますが、また別記事で書こうと思います。)

実装時の注意点

アニメーションの適用対象を考える

アニメーションの適用対象について考えます。
サンプルではList表示の時に表示されるテキストをラップするColumnに、animateBoundsが指定されています。
これを外してみましょう。

AnimatedVisibility(
    !isGrid,
) {
    Column(
        modifier = Modifier
            .padding(8.dp) // ここにanimateBoundsを指定していない
    ) {
        Text(
            text = bird.id.toString(),
            style = MaterialTheme.typography.bodyMedium,
            modifier = Modifier.padding(start = 8.dp),
        )
        Text(
            text = bird.name,
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier.padding(start = 8.dp),
        )
    }

Grid->List表示に変わるときのテキストの表示に注目してください。
ここで、レイアウト変更の際、テキストが短い幅で表示しきれないときに2行になろうとして、変な感じになってしまいます。
以下のようなアニメーションになります。一瞬2行表示されているのが分かるでしょうか?
(アニメーションのスピードを、後述の開発向けオプションで変更しています。)

test9.gif

なぜこうなるかというと、Columnの子であるテキストがアニメーションの対象となっていないからです。

順を追って説明します。Grid -> List切替時にテキストがアニメーションで徐々に現れますが、
アニメーション方法としては、デフォルトのfadeIn() + expandHorizontally()が適用されます。

expandHorizontallyはコンポーネントのサイズが変わります。
Android Developer公式からの引用ですが、以下のような感じです。(参考->https://developer.android.com/develop/ui/compose/animation/composables-modifiers?hl=ja)

Animation Example

これはColumnの親であるRowにModifier.animateBoundsを指定しているので、その子であるColumnはアニメーションが適用され、サイズが徐々に変化します。
一方ColumnにはModifier.animateBoundsを指定していないので、テキスト自体はアニメーションの対象となっておらず、非表示状態から瞬時に幅いっぱいになります。
狭い幅のColumnの領域内に幅いっぱいのテキストを入れようとしていたため、テキストが2行になろうとしていました。

ポイントは、Modifier.animateBoundsをLayoutに指定することで、その直接の子の配置やサイズがアニメーションの対象になることです。
なのでここは、Textを包含するColumnにanimateBounds()を指定することで、テキストのComposableの部分のレイアウト変更もアニメーションの対象になります。

アニメーションはビジュアル的なものであり、動かしてみないとわからないと思うので、
いろいろ動かしながら調整するのが良いと思います。
ある程度動かすくらいなら、頭で理解するよりも、そちらの方が速いと思います。
ちょっと動かしてみて、その上でどういう理屈でそうなっているか考えると良いのではないかと思います。

ちょっとこの辺自分も迷うところがあるので、もう少し整理させてもらって、改めて記事を書こうと思います。

開発向けオプションの Animator再生時間スケールを変える

さて、アニメーションは早くて、デバッグしても違いが分かりづらい、ということがあると思います。
そういうときは、開発者向けオプションのAnimator再生時間スケールを変えるといいです。

7.png

ゆっくりアニメーションされるので、デバッグしやすくなります。

スケールはオフ、0.5x、1x、1.5x、2x、5x、10xの7種類から選べます。

まとめ

Modifier.animateBoundsを使うことで、リストやグリッドなど複数レイアウト間の切り替えを滑らかにアニメーションできます。
実際のアプリ要件でも活用できる場面が多いので、ぜひ試してみてください。

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?