/ Jetpack Compose  Android动画  Compose动画  AnimatedVisibility  共享元素过渡  性能优化  Android开发  声明式UI 

Jetpack Compose 动画实战:从基础 API 到性能优化全解析


封面

为什么选择 Jetpack Compose 动画系统?

传统 Android 动画(Property Animator、ViewPropertyAnimator)需要大量样板代码,与 View 体系高度耦合。Jetpack Compose 的动画系统从设计之初就与声明式 UI 深度集成,让动画与状态变化自然绑定,代码量减少 60% 以上,同时获得更好的可组合性与可测试性。

  • 声明式动画:动画跟随状态自动触发,无需手动启动/停止

  • 协程原生支持:通过 suspend 函数控制动画时序

  • 低 GC 压力:基于 State 的更新路径,减少对象分配

  • 开箱即用的物理动画:弹簧(spring)、衰减(decay)效果内置支持

核心 API 速览:从简单到复杂

Compose 动画 API 分为高级 API 和低级 API 两层,理解各自的适用场景是掌握动画系统的关键。

高级 API

  • animateContentSize:Modifier 级别的尺寸变化动画,一行代码搞定

  • AnimatedVisibility:控制组件的显示/隐藏,支持自定义进出场动画

  • AnimatedContent:内容切换时的过渡动画,支持 ContentTransform 自定义

  • Crossfade:简单的淡入淡出内容切换

// AnimatedVisibility 示例
var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
    exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
) {
    Card(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Text("我是动画卡片", modifier = Modifier.padding(16.dp))
    }
}

Button(onClick = { visible = !visible }) {
    Text(if (visible) "隐藏" else "显示")
}

低级 API

  • animate*AsState:将普通值转换为动画值,如 animateFloatAsStateanimateDpAsState

  • updateTransition:管理多个动画值的状态机,适合多属性同步动画

  • rememberInfiniteTransition:无限循环动画,适合加载指示器、呼吸灯效果

  • Animatable:命令式动画控制,与协程配合实现复杂时序

// animate*AsState 示例:颜色与大小联动
var selected by remember { mutableStateOf(false) }
val bgColor by animateColorAsState(
    targetValue = if (selected) MaterialTheme.colorScheme.primaryContainer
                  else MaterialTheme.colorScheme.surface,
    animationSpec = tween(durationMillis = 300)
)
val scale by animateFloatAsState(
    targetValue = if (selected) 1.05f else 1.0f,
    animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
)

Box(
    modifier = Modifier
        .scale(scale)
        .background(bgColor, RoundedCornerShape(12.dp))
        .clickable { selected = !selected }
        .padding(16.dp)
) {
    Text("点我试试", style = MaterialTheme.typography.bodyLarge)
}

实战场景一:列表项进场动画

列表是 Android 应用中最常见的 UI 形态,为列表项添加错落进场动画可以显著提升用户感知质量。以下是基于 LazyColumn 实现的交错动画方案:

@Composable
fun AnimatedListItem(
    item: String,
    index: Int,
    modifier: Modifier = Modifier
) {
    var visible by remember { mutableStateOf(false) }
    
    LaunchedEffect(Unit) {
        delay(index * 50L) // 错落延迟
        visible = true
    }
    
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(tween(300)) + slideInHorizontally(
            animationSpec = tween(300, easing = FastOutSlowInEasing),
            initialOffsetX = { it / 2 }
        ),
        modifier = modifier
    ) {
        ListItem(
            headlineContent = { Text(item) },
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp, vertical = 4.dp)
        )
    }
}

@Composable
fun AnimatedLazyList(items: List<String>) {
    LazyColumn {
        itemsIndexed(items) { index, item ->
            AnimatedListItem(item = item, index = index)
        }
    }
}

注意:LaunchedEffect(Unit) 确保动画只在组件首次进入组合时触发,避免重组时重复播放。

实战场景二:共享元素过渡(Compose 1.7+)

共享元素过渡(Shared Element Transition)是 Material 3 设计语言中的核心体验,Compose 1.7 正式稳定了这一 API。

// 在 SharedTransitionLayout 内使用
@Composable
fun SharedTransitionLayout(content: @Composable SharedTransitionScope.() -> Unit) {
    // 官方 API,需要 androidx.compose.animation 1.7+
}

// 列表页
@Composable
fun SharedTransitionScope.ThumbnailItem(
    item: PhotoItem,
    animatedVisibilityScope: AnimatedVisibilityScope,
    onClick: () -> Unit
) {
    Image(
        painter = rememberAsyncImagePainter(item.url),
        contentDescription = null,
        modifier = Modifier
            .sharedElement(
                state = rememberSharedContentState(key = "image-${item.id}"),
                animatedVisibilityScope = animatedVisibilityScope
            )
            .size(80.dp)
            .clip(RoundedCornerShape(8.dp))
            .clickable(onClick = onClick)
    )
}

// 详情页使用相同的 key 即可实现平滑过渡

性能优化:避免过度重组

动画性能问题往往源于不必要的重组(Recomposition)。以下是经过实践验证的优化策略:

  • 使用 derivedStateOf:当动画值被多个 Composable 读取时,用 derivedStateOf 减少传播范围

  • lambda 下沉:将读取动画状态的代码下沉到 Modifier lambda 中,只触发 Layout/Draw 阶段而非整个重组

  • 避免在动画中读取非稳定对象:确保数据类实现 @Stable 或使用 @Immutable 标注

  • 使用 graphicsLayer:平移、旋转、缩放等变换通过 graphicsLayer 实现,绕过 Layout 阶段直接走 Draw

// ✅ 推荐:lambda 下沉,只触发 graphicsLayer 重绘
val scale by animateFloatAsState(targetValue = targetScale)

Box(
    modifier = Modifier.graphicsLayer {
        scaleX = scale  // 在 lambda 内读取,不触发外部重组
        scaleY = scale
    }
)

// ❌ 避免:在外部读取,导致整个 Box 重组
Box(modifier = Modifier.scale(scale)) // 每帧都重组

通过 Layout Inspector 的 Recomposition 高亮功能可以直观发现热点区域,配合 Compose 编译器指标报告(freeCompilerArgs += "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=...")可以系统性地优化整个界面的重组效率。

发布评论

热门评论区: