- Jetpack Compose 【一】入門:擁抱現代 Android UI 開發
- Jetpack Compose 【二】狀態管理詳解
- Jetpack Compose 【三】附帶效應、協程與異步
- Jetpack Compose 【四】動畫
- Jetpack Compose【五】 高級布局與繪制技巧
- Jetpack Compose【六】終極:聲明式 UI 如何重塑開發者的思維
一、傳統動畫與 Compose 動畫的區別
在傳統的 Android View 系統中,動畫通常需要通過 ViewPropertyAnimator
、ObjectAnimator
或 ValueAnimator
等 API 來實現。這些動畫 API 為開發者提供了屬性動畫、幀動畫等功能,可以對視圖的屬性(如位置、透明度、大小等)進行動畫化。然而,這些動畫 API 的使用往往較為復雜,需要手動控制動畫的生命周期、插值器等細節,且需要配合 View
的布局和狀態管理。
與此不同,Jetpack Compose 提供了一種更加聲明式和簡潔的方式來處理動畫。在 Compose 中,動畫通過狀態驅動,開發者只需關注數據的變化,而 Compose 會自動根據數據變化更新 UI 并應用動畫效果。Compose 的動畫 API 設計上更加簡潔,通常只需要幾個方法就能實現復雜的動畫效果。
傳統動畫(View 系統):
- 需要顯式創建和管理動畫對象。
- 通過
View
的屬性動畫對單個視圖進行動畫化。 - 通常需要額外的狀態管理來控制動畫執行與生命周期。
- 動畫效果需要手動計算與插值,過程較為繁瑣。
Compose 動畫(Jetpack Compose):
- 使用聲明式編程,動畫基于狀態變化自動觸發。
- 通過
animate*AsState
系列 API、Transition
、AnimatedVisibility
等直接對 UI 元素進行動畫。 - 動畫生命周期由 Compose 框架管理,自動執行與停止。
- 開發者無需手動計算動畫過程,只需設置目標狀態和動畫屬性。
因此,Compose 動畫不僅簡化了動畫實現的流程,還增強了動畫和狀態的緊密結合,極大地提升了開發效率。
二、Compose 動畫的實現方式
2.1 AnimateXxxAsState
系列
animate*AsState
系列動畫是 Compose 中最常見的動畫方式,它允許我們動畫化元素的某些屬性,如尺寸、顏色和位置等。
示例:尺寸、顏色動畫
@Composable
fun AnimatedXXAsStateExample() {
var expanded by remember { mutableStateOf(false) }
//尺寸變化
val size by animateDpAsState(targetValue = if (expanded) 200.dp else 100.dp, label = "")
//顏色變化
val color by animateColorAsState(
targetValue = if (expanded) Color.Red else Color.Blue,
label = ""
)
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable { expanded = !expanded }
)
}
在這個示例中,方塊的尺寸在 isExpanded
狀態變化時平滑過渡,展示了如何使用 animateDpAsState
來實現尺寸動畫。
示例:位移動畫 (animateDpAsState
)
@Composable
fun AnimatedOffsetExample() {
var isMoved by remember { mutableStateOf(false) }
// 使用 animateDpAsState 動畫化偏移量
val offsetX by animateDpAsState(
targetValue = if (isMoved) 200.dp else 0.dp,
label = "BoxOffset"
)
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.size(100.dp)
.offset(x = offsetX) // 位置設置 x 軸偏移
.background(Color.Blue)
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isMoved = !isMoved }) {
Text("Move position")
}
}
}
此示例展示了如何通過 animateDpAsState
實現方塊的平滑位移動畫。
2.2 AnimatedVisibility
AnimatedVisibility
是一個用于控制視圖可見性的動畫組件。它通過 enter
和 exit
動畫來控制視圖的顯示和隱藏,可以配置多種不同的入場和出場動畫效果,包含如下內容:
- 淡入 : fadeIn / fadeout
- 縮放 : scaleIn / scaleOut
- 滑動 : slideIn / slideOut
- 展開 : expandIn / shrinkOut
示例:淡入淡出 (fadeIn
/ fadeOut
)
@Composable
fun FadeInOutExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
)
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isVisible = !isVisible }) {
Text("切換顯示")
}
}
}
此示例演示了如何使用 fadeIn
和 fadeOut
來實現方塊的淡入淡出效果。
示例:縮放 (scaleIn
/ scaleOut
)
@Composable
fun ScaleInScaleOutExample() {
var visible by remember { mutableStateOf(true) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
AnimatedVisibility(
visible = visible,
enter = scaleIn(tween(durationMillis = 500)),
exit = scaleOut(tween(durationMillis = 500))
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
)
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { visible = !visible }) {
Text("切換可見性")
}
}
}
此例中,方塊的顯示與隱藏通過縮放動畫實現。
示例:滑動 (slideIn
/ slideOut
)
@Composable
fun SlideInOutExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(
visible = isVisible,
enter = slideInHorizontally(
initialOffsetX = { -300 } // 從左側滑入
),
exit = slideOutHorizontally(
targetOffsetX = { 300 } // 向右滑出
)
) {
Box(
modifier = Modifier
.size(120.dp)
.background(Color.Green)
)
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isVisible = !isVisible }) {
Text("滑動切換")
}
}
}
-
slideInHorizontally
:-
initialOffsetX
控制初始位置,負值表示從左側滑入。
-
-
slideOutHorizontally
:-
targetOffsetX
控制退出時的位置,正值表示向右滑出。
-
示例:enter/exit 都可以組合這4個動畫
@Composable
fun CombinedAnimationExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn() + scaleIn(initialScale = 0.5f),
exit = fadeOut() + scaleOut(targetScale = 1.5f)
) {
Box(
modifier = Modifier
.size(120.dp)
.background(Color.Magenta),
contentAlignment = Alignment.Center
) {
Text("Hello", color = Color.White)
}
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isVisible = !isVisible }) {
Text(if (isVisible) "隱藏" else "顯示")
}
}
}
該示例展示了淡入 + 縮放組合動畫。
2.3 Transition
動畫
在 Compose 中,Transition
是一種強大的動畫工具,允許開發者在多個狀態之間平滑過渡。Transition
使得開發者能夠根據不同的狀態變化定義一系列的動畫過渡效果,從而實現復雜的 UI 動畫。它特別適用于那些需要同時動畫多個屬性(如位置、尺寸、透明度等)的場景。
Transition
通過對多個目標值進行動畫處理,可以實現更為豐富和復雜的交互效果,例如在視圖狀態變化時同時對多個屬性進行過渡。
示例1:使用 Transition
實現位置 + 顏色 + 大小 組合動畫
@Composable
fun TransitionExample() {
var isExpanded by remember { mutableStateOf(false) }
// 使用 updateTransition 來處理多個屬性的動畫
val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")
// 定義動畫效果
val size by transition.animateDp(label = "Size") { state ->
if (state) 150.dp else 100.dp
}
val color by transition.animateColor(label = "Color") { state ->
if (state) Color.Red else Color.Green
}
val offset by transition.animateDp(label = "Offset") { state ->
if (state) 200.dp else 0.dp
}
Column {
Box(
modifier = Modifier
.size(size)
.offset(x = offset)
.background(color)
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isExpanded = !isExpanded }) {
Text("切換狀態")
}
}
}
在這個例子中,updateTransition
被用來處理 isExpanded
狀態的變化。當狀態從 false
變為 true
時,方塊的尺寸、顏色和位置都會同步動畫過渡。這里通過 animateDp
和 animateColor
方法分別對尺寸、顏色和位置進行動畫化。
示例 2:高級用法:多狀態切換動畫實現 3 個狀態的切換
使用枚舉定義狀態,實現多狀態之間的復雜動畫。
enum class BoxState {
Small, Medium, Large
}
@Composable
fun MultiStateTransition() {
var boxState by remember { mutableStateOf(BoxState.Small) }
// 創建多狀態 Transition
val transition = updateTransition(targetState = boxState, label = "MultiStateTransition")
// 動畫:大小變化
val boxSize by transition.animateDp(label = "BoxSize") { state ->
when (state) {
BoxState.Small -> 80.dp
BoxState.Medium -> 150.dp
BoxState.Large -> 250.dp
}
}
// 動畫:顏色變化
val boxColor by transition.animateColor(label = "BoxColor") { state ->
when (state) {
BoxState.Small -> Color.Red
BoxState.Medium -> Color.Green
BoxState.Large -> Color.Blue
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(boxSize)
.background(boxColor)
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
boxState = when (boxState) {
BoxState.Small -> BoxState.Medium
BoxState.Medium -> BoxState.Large
BoxState.Large -> BoxState.Small
}
}) {
Text("切換狀態")
}
}
}
Transition
的優勢
-
多屬性同步動畫:
Transition
使得多個屬性的動畫可以同步進行,避免了手動管理多個動畫對象。 - 簡潔聲明式:動畫過程的聲明式編程方式使得代碼更加簡潔、可讀。開發者只需關注狀態的變化,Compose 會自動處理動畫的細節。
-
動態控制:通過
updateTransition
,開發者可以在狀態變化過程中靈活調整多個屬性的動畫效果,從而打造更加豐富的交互體驗。
2.4 AnimationSpec
動畫
AnimationSpec
定義了動畫的行為,類似于傳統View體系中的差值器Interpolator
,包括動畫的速度、持續時間、緩動曲線等。它適用于所有 animate*
系列函數(如 animateDpAsState
、updateTransition
、Animatable
等),用于控制動畫的執行方式。
常用的 AnimationSpec
類型
類型 | 描述 | 適用場景 |
---|---|---|
tween() |
補間動畫,按時間線性或非線性變化 | 適用于簡單、平滑的動畫過渡 |
spring() |
彈性動畫,模擬物理世界的彈性和阻尼效果 | 適用于有彈性的動畫,如按鈕回彈 |
keyframes() |
關鍵幀動畫,定義多個時間點的動畫值 | 適用于復雜、多階段的動畫 |
snap() |
瞬間完成動畫,直接跳到目標值 | 適用于無過渡效果的快速切換 |
repeatable() |
可重復動畫,指定重復次數和方向 | 適用于循環動畫 |
infiniteRepeatable() |
無限循環動畫 | 適用于持續播放的動畫(如旋轉) |
示例1. tween()
——補間動畫
控制動畫的 時長、延遲、緩動曲線。
@Composable
fun TweenAnimation() {
var isMoved by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(
targetValue = if (isMoved) 200.dp else 0.dp,
animationSpec = tween(
durationMillis = 1000, // 動畫時長
delayMillis = 300, // 動畫延遲
easing = FastOutSlowInEasing // 緩動曲線
), label = "OffsetAnimation"
)
Column {
Box(
Modifier
.size(100.dp)
.offset(x = offsetX)
.background(Color.Blue)
)
Button(onClick = { isMoved = !isMoved }) {
Text("切換動畫")
}
}
}
tween()
參數:
-
durationMillis
:動畫持續時間(毫秒)。 -
delayMillis
:動畫開始前的延遲時間。 -
easing
:緩動效果(詳見下方緩動函數)。
示例2. spring()
——彈性動畫
模擬物理世界的 彈性效果,包括彈力和阻尼。
@Composable
fun SpringAnimation() {
var isExpanded by remember { mutableStateOf(false) }
val size by animateDpAsState(
targetValue = if (isExpanded) 200.dp else 100.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, // 阻尼比
stiffness = Spring.StiffnessLow // 剛度
), label = "SpringAnimation"
)
Column {
Box(
Modifier
.size(size)
.background(Color.Green)
)
Button(onClick = { isExpanded = !isExpanded }) {
Text("彈性動畫")
}
}
}
spring()
參數:
-
dampingRatio
:阻尼比,控制動畫的回彈程度:-
DampingRatioNoBouncy
:無回彈 -
DampingRatioLowBouncy
:輕微回彈 -
DampingRatioMediumBouncy
:中等回彈(推薦) -
DampingRatioHighBouncy
:強烈回彈
-
-
stiffness
:剛度,控制動畫速度:-
StiffnessVeryLow
:非常慢 -
StiffnessLow
:較慢 -
StiffnessMedium
:中速(默認) -
StiffnessHigh
:快速
-
示例3. keyframes()
——關鍵幀動畫
自定義動畫的各個關鍵時間點,精確控制動畫過程。
@Composable
fun KeyframesAnimation() {
var isMoved by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(
targetValue = if (isMoved) 300.dp else 0.dp,
animationSpec = keyframes {
durationMillis = 3000 // 總時長
50.dp at 500 // 0.5 秒后到 50.dp
150.dp at 1000 // 1 秒后到 150.dp
200.dp at 2000 // 2 秒后到 200.dp
}, label = "KeyframeAnimation"
)
Column {
Box(
Modifier
.size(100.dp)
.offset(x = offsetX)
.background(Color.Red)
)
Button(onClick = { isMoved = !isMoved }) {
Text("關鍵幀動畫")
}
}
}
keyframes()
參數:
-
durationMillis
:總動畫時長(必須)。 -
at
:指定關鍵幀時間點,格式為value at time
。
示例4. repeatable()
& infiniteRepeatable()
——循環動畫
@Composable
fun RepeatAnimation() {
val infiniteOffset by rememberInfiniteTransition(label = "infinite").animateFloat(
initialValue = 0f,
targetValue = 200f,
animationSpec = infiniteRepeatable(
animation = tween(1000), // 每次動畫的時長
repeatMode = RepeatMode.Reverse // 循環方式
), label = "RepeatAnimation"
)
Box(
Modifier
.size(100.dp)
.offset(x = infiniteOffset.dp)
.background(Color.Magenta)
)
}
參數:
animation
:內部使用tween()
、spring()
、keyframes()
。-
repeatMode
:-
RepeatMode.Restart
:每次重頭開始。 -
RepeatMode.Reverse
:往返播放(推薦)。
-
示例5. snap()
——瞬間動畫
瞬間完成動畫,立即切換到目標值。
@Composable
fun SnapAnimation() {
var isMoved by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(
targetValue = if (isMoved) 200.dp else 0.dp,
animationSpec = snap(delayMillis = 500), label = "SnapAnimation"
)
Column {
Box(
Modifier
.size(100.dp)
.offset(x = offsetX)
.background(Color.Cyan)
)
Button(onClick = { isMoved = !isMoved }) {
Text("瞬間切換")
}
}
}
常用 Easing
緩動函數
緩動函數 | 描述 |
---|---|
LinearEasing |
線性勻速 |
FastOutSlowInEasing |
快出慢入,Material Design 標準曲線 |
EaseIn |
慢入,適用于淡入效果 |
EaseOut |
快出,適用于淡出效果 |
EaseInOut |
先慢后快,再慢,適用于對稱動畫 |
CubicsBezierEasing |
自定義貝塞爾曲線 |