Compose1.8がリリースされました。TextのAutoSizeが追加されて話題となっていますが、本記事ではModifier.animateBounds()
について紹介します。
レイアウト変更のアニメーションが簡単にできるようになり、アニメーションの幅が広がりました。
本記事では、Modifier.animateBounds()
の基本概念と、実際に使用したサンプルコードを紹介します。
Modifier.animateBoundsとは
Modifier.animateBounds()
はコンポーネントのサイズと位置の変化をアニメーションさせることができます。
これまではサイズの変化をアニメーションするだけならModifier.animateContentSize()
を使えば良かったのですが、位置の変化のアニメーションは簡単にはできませんでした。
LookaheadScopeをつかて実装する例もありましたが、カスタムレイアウトで出てくるplaceable
とかmesureable
みたいなAPIを使う必要があり、サクッ作れる感じではなかったです。
しかしModifier.animateBounds()
を使えば簡単にできるようになります。
下記例は画像の位置とサイズをアニメーションさせています。(画像にはAI生成画像を使っています。)
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生成画像を使っています。)
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で使い所がありそうですね。