Jetpack Compose 动画:4 种技术让你的应用更具活力

发布: (2026年3月2日 GMT+8 08:18)
8 分钟阅读
原文: Dev.to

Source: Dev.to

动画是现代 Android 应用的心跳。它将静态 UI 转化为流畅、响应式的体验,让用户感受到自然且愉悦的交互。Jetpack Compose——Google 为 Android 推出的现代声明式 UI 框架,提供了强大的内置 API,只需几行代码即可创建平滑的动画。

在本指南中,我将带你了解 Compose 中的四种关键动画技术,帮助你的应用从普通提升到卓越。每种技术都配有实用的代码示例,您可以立即在项目中使用。

Source:

1. animateFloatAsState:平滑数值变化的基石

animateFloatAsState 可能是 Compose 中使用最广泛的动画 API。它能够将 float 值从当前状态平滑过渡到目标值,非常适合用于不透明度淡入淡出、缩放变化和旋转等场景。

使用案例:点击时按钮不透明度动画

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha

@Composable
fun AnimatedOpacityButton() {
    var isPressed by remember { mutableStateOf(false) }

    // Animate opacity from 1.0 to 0.5 when pressed
    val alpha by animateFloatAsState(
        targetValue = if (isPressed) 0.5f else 1.0f,
        label = "button_alpha"
    )

    Button(
        onClick = { isPressed = !isPressed },
        modifier = Modifier.alpha(alpha)
    ) {
        Text("Tap me")
    }
}

为什么有效

  • 平滑过渡 – 动画默认在 300 ms 内完成,营造自然的淡入淡出效果。
  • 状态驱动 – 每当状态条件变化时,动画会自动触发。
  • 性能优秀 – 使用底层图形 API,实现流畅的 60 fps 动画。

自定义动画速度

可以通过 animationSpec 来控制动画时长:

val alpha by animateFloatAsState(
    targetValue = if (isPressed) 0.5f else 1.0f,
    animationSpec = tween(durationMillis = 500), // 更慢的淡入淡出
    label = "button_alpha"
)

2. AnimatedVisibility:带波兰语的显示/隐藏

虽然 animateFloatAsState 处理数值变化,AnimatedVisibility 则通过优雅的进入/退出动画将可组合项从视图层次结构中加入或移除。

使用场景:动画错误信息

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun LoginForm() {
    var showError by remember { mutableStateOf(false) }
    var email by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") }
        )

        // 带滑动 + 渐变动画的错误信息
        AnimatedVisibility(
            visible = showError,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Text(
                "Invalid email format",
                color = Color.Red,
                modifier = Modifier
                    .background(Color(0xFFFFEBEE))
                    .padding(12.dp)
            )
        }

        Button(onClick = {
            showError = email.isEmpty() || !email.contains("@")
        }) {
            Text("Sign In")
        }
    }
}

动画组合

Compose 允许你组合多个动画:

enter = slideInVertically() + fadeIn() + expandVertically()
exit  = slideOutVertically() + fadeOut() + shrinkVertically()

这样就能实现一种精致的效果:错误信息同时滑入、淡入并展开。

3. animateContentSize:平滑布局变化

当可组合项的大小因内容更新而改变时,animateContentSize 会对布局变化进行动画处理,而不是瞬间跳变。

使用场景:展开描述文本

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp

@Composable
fun ExpandableText(title: String, fullText: String) {
    var isExpanded by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .clickable { isExpanded = !isExpanded }
            .animateContentSize(animationSpec = tween(durationMillis = 400))
            .padding(16.dp)
    ) {
        Text(
            text = title,
            style = MaterialTheme.typography.headlineSmall
        )

        Text(
            text = fullText,
            maxLines = if (isExpanded) Int.MAX_VALUE else 3,
            overflow = TextOverflow.Ellipsis
        )
    }
}

随意将这些代码片段复制到自己的项目中,并尝试调整参数,以匹配你的应用设计语言。

魔法

maxLines3 更改为 MAX_VALUE 时,列的高度平滑展开。没有突兀的跳动——只有优雅的增长。

Source:

4. updateTransition + Crossfade:复杂状态动画

对于涉及多个属性同时变化的更复杂动画,updateTransition 将所有动画作为单一逻辑单元进行编排。

使用场景:带有旋转指示器的加载状态

import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

enum class LoadingState {
    Idle, Loading, Success, Error
}

@Composable
fun LoadingIndicator(state: LoadingState) {
    val transition = updateTransition(targetState = state, label = "loading_transition")

    val rotation by transition.animateFloat(label = "rotation") {
        if (it == LoadingState.Loading) 360f else 0f
    }

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Crossfade(targetState = state, label = "content_crossfade") { loadingState ->
            when (loadingState) {
                LoadingState.Idle -> Text("Ready to load")
                LoadingState.Loading -> CircularProgressIndicator(
                    modifier = Modifier
                        .size(40.dp)
                        .rotate(rotation)
                )
                LoadingState.Success -> Text("Loaded!", color = Color.Green)
                LoadingState.Error -> Text("Error occurred", color = Color.Red)
            }
        }
    }
}

为什么 updateTransition 如此强大

  • 编排 – 所有与同一状态变化绑定的动画同步进行。
  • 一致性 – 防止动画相互冲突。
  • 性能 – 仅在动画值变化时重新组合,而不是在每一帧都重新组合。

进阶:使用 Crossfade 实现优雅的内容切换

Crossfade 通过让旧的 composable 渐隐、新的 composable 渐显来实现两者之间的过渡。

@Composable
fun TabContent(selectedTab: Int) {
    Crossfade(targetState = selectedTab, label = "tab_switch") { tab ->
        when (tab) {
            0 -> HomeScreen()
            1 -> ProfileScreen()
            else -> SettingsScreen()
        }
    }
}

无需编写复杂的进入/退出逻辑——Crossfade 会自动处理视觉上的优雅切换。

性能提示

  • 使用 label 参数 – 有助于在 Android Studio 的 Animation Inspector 中进行调试。
  • 对简单数值使用 animateFloatAsState – 它经过优化且轻量。
  • 避免在初始组合期间进行动画 – 动画开始前状态必须保持稳定。
  • 在真实设备上测试 – 模拟器无法准确反映动画性能。

综合运用

最完善的应用会结合使用这些技术:

TechniqueIdeal Use
animateFloatAsState快速状态反馈(按钮点击、切换)
AnimatedVisibility进入/退出屏幕和对话框
animateContentSize内容自然增长(展开列表、文本)
updateTransition复杂的同步状态变化

每种技术都针对特定的动画问题,掌握这四种技术后,你的 Compose 应用将带来真正令人愉悦的体验。

下一步

所有 8 个模板都使用干净的 Compose UI,已准备好进行动画:

首先在你的下一个功能中实现 animateFloatAsState,然后随着应用复杂度的提升,逐步探索其他技术。

祝动画愉快!

0 浏览
Back to Blog

相关文章

阅读更多 »