Jetpack Compose 【四】動畫

一、傳統(tǒng)動畫與 Compose 動畫的區(qū)別

在傳統(tǒng)的 Android View 系統(tǒng)中,動畫通常需要通過 ViewPropertyAnimator、ObjectAnimatorValueAnimator 等 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)):

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

Compose 動畫(Jetpack Compose):

  1. 使用聲明式編程,動畫基于狀態(tài)變化自動觸發(fā)。
  2. 通過 animate*AsState 系列 API、TransitionAnimatedVisibility 等直接對 UI 元素進行動畫。
  3. 動畫生命周期由 Compose 框架管理,自動執(zhí)行與停止。
  4. 開發(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 是一個用于控制視圖可見性的動畫組件。它通過 enterexit 動畫來控制視圖的顯示和隱藏,可以配置多種不同的入場和出場動畫效果,包含如下內(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("切換顯示")
        }
    }
}

此示例演示了如何使用 fadeInfadeOut 來實現(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 時,方塊的尺寸、顏色和位置都會同步動畫過渡。這里通過 animateDpanimateColor 方法分別對尺寸、顏色和位置進行動畫化。

示例 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、updateTransitionAnimatable 等),用于控制動畫的執(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 自定義貝塞爾曲線
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容