Jetpack Compose 1.8 で追加されたModifier.animateBounds
は、レイアウト変更を滑らかにアニメーションできる便利な API です。
前回の記事(Compose Animation 1.8 で追加された Modifier.animateBounds を使ってみる)では、基本的な使い方を紹介しましたが、ただ使ってみたというだけの、どこで使うUIなんだというものでした。今回はより実践的な UI 例と、実装時の工夫・注意点をまとめます。
なお、前回同様、画像にはAI生成画像を使っています。
目次
実践的な UI 実装例
例えば、アプリでよく見かける「リスト表示とグリッド表示の切り替え」をアニメーション付きで実現したい場合、Modifier.animateBounds
が非常に役立ちます。
アニメーションはありませんが、DailyArtというアプリでこのUIを見かけたので、興味があればご覧ください。
- List 表示時は画像+テキスト、Grid 表示時は画像のみを表示
- レイアウト切り替え時に、画像やテキストの位置・サイズが滑らかに変化
実際の動きは以下の 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
がついたコンポーネントのサイズや位置を徐々にアニメーションで変化させる、という理解です。
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行表示されているのが分かるでしょうか?
(アニメーションのスピードを、後述の開発向けオプションで変更しています。)
なぜこうなるかというと、Columnの子であるテキストがアニメーションの対象となっていないからです。
順を追って説明します。Grid -> List切替時にテキストがアニメーションで徐々に現れますが、
アニメーション方法としては、デフォルトのfadeIn() + expandHorizontally()
が適用されます。
expandHorizontallyはコンポーネントのサイズが変わります。
Android Developer公式からの引用ですが、以下のような感じです。(参考->https://developer.android.com/develop/ui/compose/animation/composables-modifiers?hl=ja)
これはColumnの親であるRowにModifier.animateBounds
を指定しているので、その子であるColumnはアニメーションが適用され、サイズが徐々に変化します。
一方ColumnにはModifier.animateBounds
を指定していないので、テキスト自体はアニメーションの対象となっておらず、非表示状態から瞬時に幅いっぱいになります。
狭い幅のColumnの領域内に幅いっぱいのテキストを入れようとしていたため、テキストが2行になろうとしていました。
ポイントは、Modifier.animateBounds
をLayoutに指定することで、その直接の子の配置やサイズがアニメーションの対象になることです。
なのでここは、Textを包含するColumnにanimateBounds()
を指定することで、テキストのComposableの部分のレイアウト変更もアニメーションの対象になります。
アニメーションはビジュアル的なものであり、動かしてみないとわからないと思うので、
いろいろ動かしながら調整するのが良いと思います。
ある程度動かすくらいなら、頭で理解するよりも、そちらの方が速いと思います。
ちょっと動かしてみて、その上でどういう理屈でそうなっているか考えると良いのではないかと思います。
ちょっとこの辺自分も迷うところがあるので、もう少し整理させてもらって、改めて記事を書こうと思います。
開発向けオプションの Animator再生時間スケールを変える
さて、アニメーションは早くて、デバッグしても違いが分かりづらい、ということがあると思います。
そういうときは、開発者向けオプションのAnimator再生時間スケールを変えるといいです。

ゆっくりアニメーションされるので、デバッグしやすくなります。
スケールはオフ、0.5x、1x、1.5x、2x、5x、10xの7種類から選べます。
まとめ
Modifier.animateBounds
を使うことで、リストやグリッドなど複数レイアウト間の切り替えを滑らかにアニメーションできます。
実際のアプリ要件でも活用できる場面が多いので、ぜひ試してみてください。