Android Compose UI Animations 動畫
在 Jetpack Compose 中,動畫不再是繁瑣的 XML 定義或命令式的 startAnimation()。動畫的核心思維是宣告式的:你只需要定義「目標狀態」,Compose 的動畫系統就會自動處理狀態改變時的過場效果。
本文涵蓋了從基礎的屬性轉換到進階的手勢驅動動畫,協助你建構流暢且具備物理真實感的現代化 UI。
屬性動畫 (animate*AsState)
這是最直觀且最常用的動畫 API。它能將一個基準 State 轉換成「動畫化的 State」。當 targetValue 改變時,回傳的 value 會平滑地過渡。
var isActivated by remember { mutableStateOf(false) }
// 動畫色彩
val backgroundColor by animateColorAsState(
targetValue = if (isActivated) Color.Green else Color.Gray,
label = "ColorAnimation"
)
// 動畫尺寸
val size by animateDpAsState(
targetValue = if (isActivated) 200.dp else 100.dp,
label = "SizeAnimation"
)
Box(
modifier = Modifier
.size(size)
.background(backgroundColor)
.clickable { isActivated = !isActivated }
)
動畫規格 (AnimationSpec)
你可以透過 animationSpec 參數來自訂過渡的質感:
tween:傳統的持續時間插值(可設定 Easing)。spring:基於物理的彈簧動畫(官方推薦,因為它最自然)。keyframes:精確控制各個時間點的數值。
val size by animateDpAsState(
targetValue = if (expanded) 200.dp else 100.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBounce, // 彈跳程度
stiffness = Spring.StiffnessLow // 剛性 (速度)
)
)
佈局過渡動畫
AnimatedVisibility (元件存滅)
處理元件進入或退出畫面時的動畫。你可以自由組合多種效果:
AnimatedVisibility(
visible = isVisible,
enter = slideInHorizontally() + fadeIn(), // 從側面滑入 + 淡入
exit = slideOutHorizontally() + fadeOut() // 向側面滑出 + 淡出
) {
Box(Modifier.fillMaxWidth().height(100.dp).background(Color.Red))
}
AnimatedContent (內容切換)
當一個 Composable 內的內容(如數字、圖片)改變時,使用這款 API 來定義舊內容如何離開、新內容如何進入。
var count by remember { mutableStateOf(0) }
AnimatedContent(
targetState = count,
transitionSpec = {
// 使用 slideIntoContainer 手法簡化代碼
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) togetherWith
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Up)
}
) { targetCount ->
Text("目前數字: $targetCount", style = MaterialTheme.typography.displayLarge)
}
animateContentSize (尺寸自動變化)
如果你有一個 Column 或 Row,其內容大小會動態改變,只要在 Modifier 加上這行,佈局的調整就會變得非常平滑。
Box(
modifier = Modifier
.background(Color.LightGray)
.animateContentSize() // 只要內部內容變大,外殼就會平滑變大
) {
Text(
text = if (isExpanded) longText else shortText,
modifier = Modifier.padding(16.dp)
)
}
進階動畫控制
當簡單的目標動畫無法滿足需求時,Compose 提供了更強大的底層 API。
updateTransition (多屬性同步動畫)
如果你有多個動畫屬性(如:顏色、尺寸、旋轉角度)都取決於同一個狀態,updateTransition 是最佳選擇。它能統一追蹤狀態改變,並讓所有屬性同步過渡。
enum class BoxState { Small, Large }
var currentState by remember { mutableStateOf(BoxState.Small) }
val transition = updateTransition(currentState, label = "BoxTransition")
val size by transition.animateDp(label = "Size") { state ->
if (state == BoxState.Small) 50.dp else 150.dp
}
val color by transition.animateColor(label = "Color") { state ->
if (state == BoxState.Small) Color.Blue else Color.Magenta
}
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable { currentState = if (currentState == BoxState.Small) BoxState.Large else BoxState.Small }
)
Animatable (底層命令式 API)
Animatable 允許你在 Coroutine 中直接呼叫 animateTo(),提供最極致的控制權,適合處理連續或複雜的邏輯。
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(isError) {
if (isError) {
color.animateTo(Color.Red, animationSpec = tween(500))
color.animateTo(Color.Gray, animationSpec = tween(500))
}
}
InfiniteTransition (循環動畫)
用於建立永不停止的動畫,例如呼吸燈效果或旋轉圖示。
val infiniteTransition = rememberInfiniteTransition(label = "Infinite")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.2f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "Alpha"
)
手勢與動畫的整合
在 Compose 中,手勢驅動動畫 (Gesture-driven Animation) 讓 UI 具備與使用者互動的真實感。
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
.size(80.dp)
.background(Color.Blue, CircleShape)
)
效能與除錯建議
- 使用
graphicsLayer:對於 Alpha、Scale、Rotation 等屬性,優先在graphicsLayer中設定,避免不必要的重組 (Recomposition)。 - Lambda 版 Modifier:頻繁變動的數值(如 Offset)建議使用 lambda 版本以優化效能。
- 善用 Label:填寫動畫的
label參數,以便在 Android Studio 的 Animation Preview 工具中進行視覺化除錯。
好的動畫應該是「隱形」的。它們應該感覺自然且反應靈敏,不應分散使用者的注意力。優先選擇 Spring 物理動畫能讓你的 App 質感瞬間提升。