- Jetpack Compose 【一】入門:擁抱現(xiàn)代 Android UI 開發(fā)
- Jetpack Compose 【二】狀態(tài)管理詳解
- Jetpack Compose 【三】附帶效應、協(xié)程與異步
- Jetpack Compose 【四】動畫
- Jetpack Compose【五】 高級布局與繪制技巧
- Jetpack Compose【六】終極:聲明式 UI 如何重塑開發(fā)者的思維
一、傳統(tǒng)動畫與 Compose 動畫的區(qū)別
在傳統(tǒng)的 Android View 系統(tǒng)中,動畫通常需要通過 ViewPropertyAnimator、ObjectAnimator 或 ValueAnimator 等 API 來實現(xiàn)。這些動畫 API 為開發(fā)者提供了屬性動畫、幀動畫等功能,可以對視圖的屬性(如位置、透明度、大小等)進行動畫化。然而,這些動畫 API 的使用往往較為復雜,需要手動控制動畫的生命周期、插值器等細節(jié),且需要配合 View 的布局和狀態(tài)管理。
與此不同,Jetpack Compose 提供了一種更加聲明式和簡潔的方式來處理動畫。在 Compose 中,動畫通過狀態(tài)驅(qū)動,開發(fā)者只需關(guān)注數(shù)據(jù)的變化,而 Compose 會自動根據(jù)數(shù)據(jù)變化更新 UI 并應用動畫效果。Compose 的動畫 API 設計上更加簡潔,通常只需要幾個方法就能實現(xiàn)復雜的動畫效果。
傳統(tǒng)動畫(View 系統(tǒng)):
- 需要顯式創(chuàng)建和管理動畫對象。
- 通過
View的屬性動畫對單個視圖進行動畫化。 - 通常需要額外的狀態(tài)管理來控制動畫執(zhí)行與生命周期。
- 動畫效果需要手動計算與插值,過程較為繁瑣。
Compose 動畫(Jetpack Compose):
- 使用聲明式編程,動畫基于狀態(tài)變化自動觸發(fā)。
- 通過
animate*AsState系列 API、Transition、AnimatedVisibility等直接對 UI 元素進行動畫。 - 動畫生命周期由 Compose 框架管理,自動執(zhí)行與停止。
- 開發(fā)者無需手動計算動畫過程,只需設置目標狀態(tài)和動畫屬性。
因此,Compose 動畫不僅簡化了動畫實現(xiàn)的流程,還增強了動畫和狀態(tài)的緊密結(jié)合,極大地提升了開發(fā)效率。
二、Compose 動畫的實現(xiàn)方式
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 狀態(tài)變化時平滑過渡,展示了如何使用 animateDpAsState 來實現(xiàn)尺寸動畫。
示例:位移動畫 (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 實現(xiàn)方塊的平滑位移動畫。
2.2 AnimatedVisibility
AnimatedVisibility 是一個用于控制視圖可見性的動畫組件。它通過 enter 和 exit 動畫來控制視圖的顯示和隱藏,可以配置多種不同的入場和出場動畫效果,包含如下內(nèi)容:
- 淡入 : 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 來實現(xiàn)方塊的淡入淡出效果。
示例:縮放 (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("切換可見性")
}
}
}
此例中,方塊的顯示與隱藏通過縮放動畫實現(xiàn)。
示例:滑動 (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 } // 從左側(cè)滑入
),
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控制初始位置,負值表示從左側(cè)滑入。
-
-
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 是一種強大的動畫工具,允許開發(fā)者在多個狀態(tài)之間平滑過渡。Transition 使得開發(fā)者能夠根據(jù)不同的狀態(tài)變化定義一系列的動畫過渡效果,從而實現(xiàn)復雜的 UI 動畫。它特別適用于那些需要同時動畫多個屬性(如位置、尺寸、透明度等)的場景。
Transition 通過對多個目標值進行動畫處理,可以實現(xiàn)更為豐富和復雜的交互效果,例如在視圖狀態(tài)變化時同時對多個屬性進行過渡。
示例1:使用 Transition 實現(xiàn)位置 + 顏色 + 大小 組合動畫
@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("切換狀態(tài)")
}
}
}
在這個例子中,updateTransition 被用來處理 isExpanded 狀態(tài)的變化。當狀態(tài)從 false 變?yōu)?true 時,方塊的尺寸、顏色和位置都會同步動畫過渡。這里通過 animateDp 和 animateColor 方法分別對尺寸、顏色和位置進行動畫化。
示例 2:高級用法:多狀態(tài)切換動畫實現(xiàn) 3 個狀態(tài)的切換
使用枚舉定義狀態(tài),實現(xiàn)多狀態(tài)之間的復雜動畫。
enum class BoxState {
Small, Medium, Large
}
@Composable
fun MultiStateTransition() {
var boxState by remember { mutableStateOf(BoxState.Small) }
// 創(chuàng)建多狀態(tài) 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("切換狀態(tài)")
}
}
}
Transition 的優(yōu)勢
-
多屬性同步動畫:
Transition使得多個屬性的動畫可以同步進行,避免了手動管理多個動畫對象。 - 簡潔聲明式:動畫過程的聲明式編程方式使得代碼更加簡潔、可讀。開發(fā)者只需關(guān)注狀態(tài)的變化,Compose 會自動處理動畫的細節(jié)。
-
動態(tài)控制:通過
updateTransition,開發(fā)者可以在狀態(tài)變化過程中靈活調(diào)整多個屬性的動畫效果,從而打造更加豐富的交互體驗。
2.4 AnimationSpec動畫
AnimationSpec 定義了動畫的行為,類似于傳統(tǒng)View體系中的差值器Interpolator,包括動畫的速度、持續(xù)時間、緩動曲線等。它適用于所有 animate* 系列函數(shù)(如 animateDpAsState、updateTransition、Animatable 等),用于控制動畫的執(zhí)行方式。
常用的 AnimationSpec 類型
| 類型 | 描述 | 適用場景 |
|---|---|---|
tween() |
補間動畫,按時間線性或非線性變化 | 適用于簡單、平滑的動畫過渡 |
spring() |
彈性動畫,模擬物理世界的彈性和阻尼效果 | 適用于有彈性的動畫,如按鈕回彈 |
keyframes() |
關(guān)鍵幀動畫,定義多個時間點的動畫值 | 適用于復雜、多階段的動畫 |
snap() |
瞬間完成動畫,直接跳到目標值 | 適用于無過渡效果的快速切換 |
repeatable() |
可重復動畫,指定重復次數(shù)和方向 | 適用于循環(huán)動畫 |
infiniteRepeatable() |
無限循環(huán)動畫 | 適用于持續(xù)播放的動畫(如旋轉(zhuǎn)) |
示例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() 參數(shù):
-
durationMillis:動畫持續(xù)時間(毫秒)。 -
delayMillis:動畫開始前的延遲時間。 -
easing:緩動效果(詳見下方緩動函數(shù))。
示例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() 參數(shù):
-
dampingRatio:阻尼比,控制動畫的回彈程度:-
DampingRatioNoBouncy:無回彈 -
DampingRatioLowBouncy:輕微回彈 -
DampingRatioMediumBouncy:中等回彈(推薦) -
DampingRatioHighBouncy:強烈回彈
-
-
stiffness:剛度,控制動畫速度:-
StiffnessVeryLow:非常慢 -
StiffnessLow:較慢 -
StiffnessMedium:中速(默認) -
StiffnessHigh:快速
-
示例3. keyframes()——關(guān)鍵幀動畫
自定義動畫的各個關(guān)鍵時間點,精確控制動畫過程。
@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("關(guān)鍵幀動畫")
}
}
}
keyframes() 參數(shù):
-
durationMillis:總動畫時長(必須)。 -
at:指定關(guān)鍵幀時間點,格式為value at time。
示例4. repeatable() & infiniteRepeatable()——循環(huán)動畫
@Composable
fun RepeatAnimation() {
val infiniteOffset by rememberInfiniteTransition(label = "infinite").animateFloat(
initialValue = 0f,
targetValue = 200f,
animationSpec = infiniteRepeatable(
animation = tween(1000), // 每次動畫的時長
repeatMode = RepeatMode.Reverse // 循環(huán)方式
), label = "RepeatAnimation"
)
Box(
Modifier
.size(100.dp)
.offset(x = infiniteOffset.dp)
.background(Color.Magenta)
)
}
參數(shù):
animation:內(nèi)部使用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 緩動函數(shù)
| 緩動函數(shù) | 描述 |
|---|---|
LinearEasing |
線性勻速 |
FastOutSlowInEasing |
快出慢入,Material Design 標準曲線 |
EaseIn |
慢入,適用于淡入效果 |
EaseOut |
快出,適用于淡出效果 |
EaseInOut |
先慢后快,再慢,適用于對稱動畫 |
CubicsBezierEasing |
自定義貝塞爾曲線 |