Jetpack Compose: 高度なアニメーションを段階的に
Compose の高度なアニメーションを完全解説:遷移、AnimatedVisibility、Animatable、ジェスチャー、滑らかな Android インターフェイスのためのパフォーマンス。

アニメーションは、機能的なアプリを記憶に残るユーザー体験へと変えます。Jetpack Compose は、滑らかなインターフェイスの作成を大幅に簡素化する強力な宣言的アニメーション API を提供しています。本ガイドでは、パフォーマンスが高く保守しやすいアニメーションを構築するための高度なテクニックを解説します。
このチュートリアルでは、Compose の基礎(リコンポジション、状態、Modifier)を理解していることを前提とします。基礎を確認するには、まず Jetpack Compose の面接質問ガイドを参照することをおすすめします。
Compose におけるアニメーション API の基礎
Compose は、アニメーション向けに複数のレベルの API を提供しています。選択は、求める制御の度合いとアニメーションの複雑さによって決まります。
API は大きく 3 つのカテゴリに分かれます。高レベルアニメーション(AnimatedVisibility、AnimatedContent)、状態ベースのアニメーション(animate*AsState)、低レベルアニメーション(Animatable、Transition)です。
// Overview of the three animation API levels
@Composable
fun AnimationApiOverview() {
// High level: simple predefined animations
AnimatedVisibility(visible = isVisible) {
Text("Animated content")
}
// Intermediate level: state-driven animation
val alpha by animateFloatAsState(
targetValue = if (isSelected) 1f else 0.5f,
label = "alpha"
)
// Low level: full control over animation
val animatable = remember { Animatable(0f) }
LaunchedEffect(targetValue) {
animatable.animateTo(targetValue)
}
}適切な API レベルを選ぶことは、必要な柔軟性を保ちながら可読性の高いコードを維持するうえで重要です。
AnimatedVisibility: 洗練された出現と消滅のアニメーション
AnimatedVisibility は、要素の出現と消滅をアニメーション化するための理想的な入り口です。この API は、コンテンツのコンポジションとデコンポジションを自動的に管理します。
enter パラメータと exit パラメータは、アニメーションの挙動を定義する遷移の組み合わせを受け取ります。これらの遷移は + 演算子で組み合わせることができます。
@Composable
fun ExpandableCard(
title: String,
content: String,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = modifier.clickable { isExpanded = !isExpanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Icon(
imageVector = if (isExpanded) Icons.Default.ExpandLess
else Icons.Default.ExpandMore,
contentDescription = null
)
}
// Expansion animation with fade + slide
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn(animationSpec = tween(300)) +
expandVertically(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200)) +
shrinkVertically(animationSpec = tween(200))
) {
Text(
text = content,
modifier = Modifier.padding(top = 12.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}AnimatedVisibility 内部のコンテンツは visible = true のときだけコンポーズされるため、展開可能な項目を多く持つリストでもパフォーマンスが最適化されます。
利用可能な遷移には、fadeIn/fadeOut、slideIn/slideOut、expandIn/shrinkOut、scaleIn/scaleOut などがあります。これらは自由に組み合わせて独自のエフェクトを作れます。
animate*AsState: 状態駆動のアニメーション
animate*AsState 系の関数は、プリミティブ値の変化を自動的にアニメーション化します。これは Compose で単純なアニメーションを書く際に最も慣用的なアプローチです。
各データ型に専用の関数があります:animateColorAsState、animateFloatAsState、animateDpAsState、animateIntAsState などです。
@Composable
fun InteractiveButton(
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
// Animated background color
val backgroundColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 250),
label = "backgroundColor"
)
// Animated elevation
val elevation by animateDpAsState(
targetValue = if (isSelected) 8.dp else 2.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "elevation"
)
// Animated text size
val textSize by animateFloatAsState(
targetValue = if (isSelected) 18f else 14f,
label = "textSize"
)
Surface(
modifier = modifier.clickable(onClick = onClick),
color = backgroundColor,
shadowElevation = elevation,
shape = RoundedCornerShape(12.dp)
) {
Text(
text = if (isSelected) "Selected" else "Select",
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
fontSize = textSize.sp
)
}
}animationSpec パラメータは、アニメーションの時間的な挙動を制御します。よく使われる 2 つの spec は、tween(イージング付きの固定時間)と spring(バウンドを伴う現実的な物理)です。
Transition: 複数のアニメーションをオーケストレーションする
複数のプロパティを協調的にアニメーション化する必要がある場合、updateTransition が中央集約的な制御を提供します。この API はすべてのアニメーションが同期し続けることを保証します。
パターンは、enum の状態を定義し、その状態に依存する各プロパティに対するアニメーションを作成することです。
// Card state: defines visual behavior
enum class CardState { Collapsed, Expanded, Selected }
@Composable
fun AnimatedStateCard(
cardState: CardState,
modifier: Modifier = Modifier
) {
// Central transition coordinating all animations
val transition = updateTransition(
targetState = cardState,
label = "cardTransition"
)
// Card height based on state
val cardHeight by transition.animateDp(
transitionSpec = { spring(stiffness = Spring.StiffnessLow) },
label = "height"
) { state ->
when (state) {
CardState.Collapsed -> 80.dp
CardState.Expanded -> 200.dp
CardState.Selected -> 160.dp
}
}
// Border color based on state
val borderColor by transition.animateColor(
transitionSpec = { tween(300) },
label = "borderColor"
) { state ->
when (state) {
CardState.Collapsed -> Color.Transparent
CardState.Expanded -> MaterialTheme.colorScheme.outline
CardState.Selected -> MaterialTheme.colorScheme.primary
}
}
// Corner radius based on state
val cornerRadius by transition.animateDp(
label = "cornerRadius"
) { state ->
when (state) {
CardState.Collapsed -> 8.dp
CardState.Expanded -> 16.dp
CardState.Selected -> 24.dp
}
}
Card(
modifier = modifier
.height(cardHeight)
.border(2.dp, borderColor, RoundedCornerShape(cornerRadius)),
shape = RoundedCornerShape(cornerRadius)
) {
// Card content
}
}Androidの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
Animatable: アニメーションの完全な制御
Animatable は、完全なプログラム的制御を提供する低レベル API です。中断可能なアニメーション、ジェスチャー、複雑なシナリオでは、このアプローチが必要となります。
animate*AsState と異なり、Animatable では、進行中のアニメーションの完了を待たずに停止、反転、変更が可能です。
@Composable
fun SwipeableCard(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
// Horizontal offset controlled by Animatable
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
// Swipe threshold to trigger dismissal
val dismissThreshold = 300f
Box(
modifier = modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
scope.launch {
if (abs(offsetX.value) > dismissThreshold) {
// Animate out then callback
val target = if (offsetX.value > 0) 1000f else -1000f
offsetX.animateTo(
targetValue = target,
animationSpec = tween(200)
)
onDismiss()
} else {
// Return to initial position with spring
offsetX.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy
)
)
}
}
},
onHorizontalDrag = { _, dragAmount ->
scope.launch {
// snapTo for instant finger tracking
offsetX.snapTo(offsetX.value + dragAmount)
}
}
)
}
) {
content()
}
}Animatable の主要なメソッドは、animateTo()(目標値へのアニメーション)、snapTo()(即時変更)、stop()(中断)です。
AnimatedContent: コンテンツの遷移
AnimatedContent は、異なるコンテンツ間の遷移をアニメーション化します。この API は、表示される UI を完全に変える状態変化に最適です。
キー targetState は、遷移をいつ発生させるかを決定します。transitionSpec は、退出するコンテンツと入場するコンテンツがどのように相互作用するかを定義します。
@Composable
fun CounterWithAnimation(
count: Int,
modifier: Modifier = Modifier
) {
AnimatedContent(
targetState = count,
modifier = modifier,
transitionSpec = {
// Determine animation direction
val direction = if (targetState > initialState) {
// New number enters from top
slideInVertically { height -> -height } + fadeIn() togetherWith
slideOutVertically { height -> height } + fadeOut()
} else {
// New number enters from bottom
slideInVertically { height -> height } + fadeIn() togetherWith
slideOutVertically { height -> -height } + fadeOut()
}
direction.using(SizeTransform(clip = false))
},
label = "counter"
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold
)
}
}AnimatedContent 内部のコンテンツは、targetState が変わるたびにリコンポーズされます。複雑なコンテンツの場合は、コストの高い要素をメモ化するか、適切なキー戦略を採用すると良いです。
rememberInfiniteTransition による無限アニメーション
ループするアニメーション(ローディングインジケータ、パルス効果など)向けに、rememberInfiniteTransition は手動でのサイクル管理を必要としない専用 API を提供します。
@Composable
fun PulsingDot(
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
// Looping scale animation
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
// Synchronized opacity animation
val alpha by infiniteTransition.animateFloat(
initialValue = 0.5f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = modifier
.size(24.dp)
.scale(scale)
.alpha(alpha)
.background(color = color, shape = CircleShape)
)
}LazyColumn のリストアニメーション
リスト項目のアニメーションには特別な配慮が必要です。animateItem() Modifier(以前は animateItemPlacement)は、並べ替えを自動的にアニメーション化します。
@Composable
fun AnimatedTaskList(
tasks: List<Task>,
onToggle: (Task) -> Unit,
onDelete: (Task) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = tasks,
key = { it.id } // Stable key required for animateItem
) { task ->
var isVisible by remember { mutableStateOf(true) }
// Exit animation before deletion
AnimatedVisibility(
visible = isVisible,
exit = shrinkVertically() + fadeOut()
) {
TaskItem(
task = task,
onToggle = { onToggle(task) },
onDelete = {
isVisible = false
// Delay to let animation complete
},
modifier = Modifier.animateItem(
fadeInSpec = tween(300),
fadeOutSpec = tween(300),
placementSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
// Trigger deletion after animation
LaunchedEffect(isVisible) {
if (!isVisible) {
delay(300)
onDelete(task)
}
}
}
}
}安定した key パラメータがないと、animateItem はリコンポジションをまたいで項目を追跡できません。リストのインデックスではなく一意な ID を使うべきです。
Canvas アニメーション
カスタムなビジュアルエフェクトには、Canvas とアニメーション値を組み合わせることで完全な柔軟性が得られます。
@Composable
fun AnimatedProgressRing(
progress: Float, // 0f to 1f
modifier: Modifier = Modifier
) {
// Progress animation with spring for natural feel
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessVeryLow
),
label = "progress"
)
// Continuous rotation animation
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
),
label = "rotation"
)
val primaryColor = MaterialTheme.colorScheme.primary
val trackColor = MaterialTheme.colorScheme.surfaceVariant
Canvas(
modifier = modifier
.size(120.dp)
.rotate(rotation)
) {
val strokeWidth = 12.dp.toPx()
val radius = (size.minDimension - strokeWidth) / 2
// Background circle (track)
drawCircle(
color = trackColor,
radius = radius,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
// Animated progress arc
drawArc(
color = primaryColor,
startAngle = -90f,
sweepAngle = animatedProgress * 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round),
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(radius * 2, radius * 2)
)
}
}Androidの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
アニメーションのパフォーマンス最適化
最適化されていないアニメーションは、ジャンクの原因となりバッテリーを消耗させます。60 FPS を維持するためのベストプラクティスを示します。
第一の原則は、アニメーション中にアロケーションを避けることです。リコンポジションを引き起こす Modifier ではなく、graphicsLayer を使うべきです。
@Composable
fun OptimizedAnimatedCard(
isExpanded: Boolean,
modifier: Modifier = Modifier
) {
val scale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f,
label = "scale"
)
val alpha by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0.8f,
label = "alpha"
)
Card(
modifier = modifier
// ✅ graphicsLayer: GPU modifications without recomposition
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
}
// ❌ Avoid: .scale(scale).alpha(alpha)
// These modifiers trigger recompositions
) {
Text("Card content")
}
}
// Example with remembered lambda to avoid allocations
@Composable
fun OptimizedClickableItem(
onClick: () -> Unit,
content: @Composable () -> Unit
) {
// ✅ Stable remembered lambda
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick
)
) {
content()
}
}第二の重要点は、リスト内のアニメーションです。同時に走るアニメーションの数を制限し、派生計算には derivedStateOf を使うべきです。
@Composable
fun PerformantAnimatedList(
items: List<Item>,
modifier: Modifier = Modifier
) {
// Calculate once whether the list is empty
val isEmpty by remember {
derivedStateOf { items.isEmpty() }
}
LazyColumn(modifier = modifier) {
items(
items = items,
key = { it.id }
) { item ->
// Lightweight animation only on initial appearance
var hasAppeared by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
hasAppeared = true
}
val alpha by animateFloatAsState(
targetValue = if (hasAppeared) 1f else 0f,
animationSpec = tween(200),
label = "itemAlpha"
)
ItemCard(
item = item,
modifier = Modifier.graphicsLayer { this.alpha = alpha }
)
}
}
}まとめ
Jetpack Compose のアニメーションは、使いやすさと高度な制御のバランスを提供します。押さえておくべきポイントは次のとおりです:
- ✅ 複雑さに応じて適切な API レベルを選ぶ(AnimatedVisibility → animate*AsState → Animatable)
- ✅ 関連する複数のアニメーションをまとめるには
updateTransitionを使う - ✅ 自然なアニメーションには
spring、正確な持続時間にはtweenを選ぶ - ✅ リストアニメーションでは常に安定した
keyパラメータを指定する - ✅ 不要なリコンポジションを避けるため
graphicsLayerで最適化する - ✅ パフォーマンスを検証するために実機でアニメーションをテストする
Compose のアニメーションを習得することは、プロフェッショナルな Android アプリを際立たせます。これらのテクニックとパフォーマンスへの配慮を組み合わせれば、滑らかで魅力的なユーザー体験を生み出せます。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
関連記事

Jetpack Compose面接質問20選(2026年版)
Jetpack Composeの面接で頻出する20の質問を解説。リコンポジション、状態管理、副作用、ナビゲーション、パフォーマンス最適化、アーキテクチャパターンを網羅。

Kotlin 2.3 Android面接対策:名前ベースの分割代入、KMP、頻出質問を徹底解説【2026年版】
2026年のAndroid開発者面接で問われるKotlin 2.3の新機能を網羅的に解説。名前ベースの分割代入、Kotlin Multiplatform、コンテキストパラメータ、Flowとコルーチンのコード例付き。

AndroidのMVVM vs MVI:2026年に選ぶべきアーキテクチャとは?
AndroidにおけるMVVMとMVIの徹底比較。メリット・デメリット、ユースケース、そして2026年に適切なアーキテクチャを選択するための実践的ガイド。