Jetpack Compose 进阶:动画、手势、主题与导航

各位同学,欢迎来到第24章。前面我们聊了Compose的基础组件和布局,今天要讲的东西,才是真正让App“活”起来的关键。动画、手势、主题、导航,再加上ViewModel集成——说白了,就是让你的界面会动、会响应、会统一风格,还能在不同页面间优雅跳转。

我个人习惯把这一章称为“Compose的成人礼”。因为一旦你掌握了这些,你就不再是只会堆组件的初级开发者了。来,我们一个一个看。

Compose 进阶知识体系 Compose 进阶 🎬 动画 👆 手势处理 🎨 主题/Material You 🧭 Navigation 🔗 ViewModel 集成

1. Compose 动画:让界面“活”起来

Compose的动画系统,跟传统View动画完全是两码事。它基于状态驱动,你只需要告诉系统“我要从A变到B”,剩下的插值、刷新,框架全包了。

核心API就三个:

  • animateFloatAsState —— 单个数值动画,比如透明度、旋转角度
  • animateColorAsState —— 颜色过渡动画
  • AnimatedVisibility —— 进场/退场动画

我在项目中遇到过最典型的场景:点赞按钮的缩放效果。用户点一下,图标弹一下,反馈感瞬间拉满。

@Composable
fun LikeButton(isLiked: Boolean, onClick: () -> Unit) {
    val scale by animateFloatAsState(
        targetValue = if (isLiked) 1.2f else 1.0f,
        animationSpec = spring(dampingRatio = 0.5f)
    )
    IconButton(onClick = onClick) {
        Icon(
            imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.Favorite,
            contentDescription = "点赞",
            modifier = Modifier.scale(scale),
            tint = if (isLiked) Color.Red else Color.Gray
        )
    }
}
💡 我的小技巧:spring() 做弹性动画,手感比线性动画好太多。用户会觉得你的App“很跟手”。

2. 手势处理:从点击到拖拽

Compose的手势系统,我刚开始也觉得有点绕。但用熟了你会发现,它比传统View的OnTouchListener清晰得多。

常用手势修饰符:

  • clickable —— 点击,最简单
  • draggable —— 单方向拖拽
  • pointerInput —— 自定义手势,比如双指缩放

举个例子,实现一个可拖拽的卡片:

@Composable
fun DraggableCard() {
    var offsetX by remember { mutableStateOf(0f) }
    Box(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    change.consume()
                    offsetX += dragAmount.x
                }
            }
            .size(100.dp)
            .background(Color.Blue)
    )
}
⚠️ 我曾经踩过的坑:pointerInput 里忘记调用 change.consume(),结果手势事件被多个组件同时响应,界面乱跳。记住:消费事件是必须的。

3. 主题与 Material You:让App有“灵魂”

Material You 是Google在Android 12推出的设计语言。说白了,就是让系统根据壁纸颜色自动生成一套主题色。Compose天然支持这套机制。

核心思路: 定义 MaterialTheme 的颜色、字体、形状三个维度。

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    secondary = Color(0xFF625B71),
    tertiary = Color(0xFF7D5260)
)

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = LightColorScheme,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

我个人习惯把主题配置单独放在一个 Theme.kt 文件里。这样换肤、适配深色模式都方便。你想想看,如果颜色散落在各个组件里,改起来得多痛苦?

4. Compose Navigation:页面跳转的新方式

Compose Navigation 跟传统Fragment Navigation最大的区别:它没有Fragment了。每个页面就是一个Composable函数,通过NavHost来管理。

基本三步走:

  1. 定义路由(字符串常量)
  2. 创建NavHost,注册页面
  3. 用navController.navigate()跳转
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(navController) }
        composable("detail/{itemId}") { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")
            DetailScreen(itemId = itemId)
        }
    }
}
🔑 关键点: 传参用路径参数(如 detail/{itemId}),不要用序列化对象。复杂数据建议通过ViewModel共享。

5. Compose 与 ViewModel 集成:状态管理的“黄金搭档”

ViewModel在Compose里的用法,跟传统View层不太一样。核心变化是:你不再需要LiveData,直接用 StateFlowMutableState

推荐模式:

  • ViewModel 内部用 MutableStateStateFlow
  • Composable 里用 viewModel() 获取实例
  • collectAsState()by mutableStateOf 监听变化
class MyViewModel : ViewModel() {
    var count by mutableStateOf(0)
        private set

    fun increment() { count++ }
}

@Composable
fun CounterScreen(viewModel: MyViewModel = viewModel()) {
    Column {
        Text("计数: ${viewModel.count}")
        Button(onClick = { viewModel.increment() }) {
            Text("+1")
        }
    }
}

嗯,这里要注意:viewModel() 默认会跟随最近的NavBackStackEntry的生命周期。如果你在嵌套导航里用,记得指定 viewModelStoreOwner,否则可能会拿到错误的实例。

💡 我的经验: 一个页面一个ViewModel,不要复用。页面销毁时ViewModel自动清除,内存管理更干净。

6. 综合实战:一个带动画的待办事项列表

最后,我们把今天学的串起来。做一个简单的待办列表:点击添加按钮弹出输入框(带动画),长按删除(手势),主题跟随系统(Material You),页面跳转到详情(Navigation),数据存在ViewModel里。

// 伪代码示意
@Composable
fun TodoApp() {
    val navController = rememberNavController()
    MaterialTheme {
        NavHost(navController, "list") {
            composable("list") { TodoListScreen(navController) }
            composable("detail/{id}") { TodoDetailScreen() }
        }
    }
}

你看,所有知识点都融到一起了。这就是Compose的魅力——它不是一个一个孤立的API,而是一套完整的声明式UI生态。

好了,这一章的内容就到这里。代码不多,但每个点都值得你亲手敲一遍。尤其是动画和手势,光看是学不会的,得动手指。


公众号:蓝海资料掘金营,微信deep3321