Jetpack Compose 【四】動畫

一、傳統動畫與 Compose 動畫的區別

在傳統的 Android View 系統中,動畫通常需要通過 ViewPropertyAnimatorObjectAnimatorValueAnimator 等 API 來實現。這些動畫 API 為開發者提供了屬性動畫、幀動畫等功能,可以對視圖的屬性(如位置、透明度、大小等)進行動畫化。然而,這些動畫 API 的使用往往較為復雜,需要手動控制動畫的生命周期、插值器等細節,且需要配合 View 的布局和狀態管理。

與此不同,Jetpack Compose 提供了一種更加聲明式和簡潔的方式來處理動畫。在 Compose 中,動畫通過狀態驅動,開發者只需關注數據的變化,而 Compose 會自動根據數據變化更新 UI 并應用動畫效果。Compose 的動畫 API 設計上更加簡潔,通常只需要幾個方法就能實現復雜的動畫效果。

傳統動畫(View 系統):

  1. 需要顯式創建和管理動畫對象。
  2. 通過 View 的屬性動畫對單個視圖進行動畫化。
  3. 通常需要額外的狀態管理來控制動畫執行與生命周期。
  4. 動畫效果需要手動計算與插值,過程較為繁瑣。

Compose 動畫(Jetpack Compose):

  1. 使用聲明式編程,動畫基于狀態變化自動觸發。
  2. 通過 animate*AsState 系列 API、TransitionAnimatedVisibility 等直接對 UI 元素進行動畫。
  3. 動畫生命周期由 Compose 框架管理,自動執行與停止。
  4. 開發者無需手動計算動畫過程,只需設置目標狀態和動畫屬性。

因此,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 是一個用于控制視圖可見性的動畫組件。它通過 enterexit 動畫來控制視圖的顯示和隱藏,可以配置多種不同的入場和出場動畫效果,包含如下內容:

  • 淡入 : 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("切換顯示")
        }
    }
}

此示例演示了如何使用 fadeInfadeOut 來實現方塊的淡入淡出效果。

示例:縮放 (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 時,方塊的尺寸、顏色和位置都會同步動畫過渡。這里通過 animateDpanimateColor 方法分別對尺寸、顏色和位置進行動畫化。

示例 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* 系列函數(如 animateDpAsStateupdateTransitionAnimatable 等),用于控制動畫的執行方式。

常用的 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 自定義貝塞爾曲線
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容