跳转到正文
YGJoon 技术博客

Android Kotlin Compose 动画进阶

发布于:
·953 字 · 约 2 分钟·作者:
Compose 动画进阶:AnimationSpec 选型、多属性过渡、Gesture 衔接 Animatable,spring 的物理感胜过线性 tween。
Android Kotlin Compose 动画进阶文章封面
目录

动画 API 总览

API用途
animate*AsState基于状态的简单动画
updateTransition管理多个动画属性
AnimatedVisibility显隐动画
rememberInfiniteTransition循环动画
Animatable手势驱动、物理动画
AnimationSpec自定义动画曲线

复杂场景示例

1. 多属性过渡

enum class CardState { Collapsed, Expanded }

@Composable
fun ExpandableCard(state: CardState) {
    val transition = updateTransition(state, label = "cardTransition")
    val height by transition.animateDp(label = "height") {
        if (it == CardState.Expanded) 240.dp else 72.dp
    }
    val alpha by transition.animateFloat(label = "alpha") {
        if (it == CardState.Expanded) 1f else 0f
    }

    Card(modifier = Modifier.height(height)) {
        Text("Details", modifier = Modifier.alpha(alpha))
    }
}

2. Gesture + Animatable

@Composable
fun SwipeToDismiss(onDismissed: () -> Unit) {
    val offsetX = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()

    Box(modifier = Modifier
        .pointerInput(Unit) {
            detectHorizontalDragGestures(
                onDragEnd = {
                    scope.launch {
                        if (offsetX.value > size.width * 0.4f) {
                            offsetX.animateTo(size.width.toFloat(), tween(200))
                            onDismissed()
                        } else {
                            offsetX.animateTo(0f, spring())
                        }
                    }
                }
            ) { _, dragAmount ->
                scope.launch { offsetX.snapTo((offsetX.value + dragAmount).coerceAtLeast(0f)) }
            }
        }
        .offset { IntOffset(offsetX.value.roundToInt(), 0) }
    ) {
        // content
    }
}

3. 无限动画

@Composable
fun LoadingDots() {
    val infiniteTransition = rememberInfiniteTransition(label = "loading")
    val alpha by infiniteTransition.animateFloat(
        initialValue = 0.3f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(600, easing = FastOutLinearInEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "alpha"
    )
    Box(modifier = Modifier.size(12.dp).background(Color.White.copy(alpha = alpha), CircleShape))
}

AnimationSpec

示例:

val spec = keyframes {
    durationMillis = 1200
    0f at 0 with LinearOutSlowInEasing
    1.2f at 400
    0.95f at 800
    1f at 1200
}

性能优化

动画与导航

composable(
    route = "detail",
    enterTransition = { fadeIn(tween(200)) + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left) },
    exitTransition = { fadeOut(tween(200)) }
) { DetailScreen() }

调试技巧

常见问题

问题原因解决方案
动画闪烁状态更新频繁或 key 不稳定保证 remember 使用正确,设置稳定 key
手势滞后Animatable 更新频繁使用 snapTo 控制更新频率
帧率不稳动画逻辑在主线程阻塞将重运算转移到 Dispatchers.Default

实战案例:动画的”物理感”来自 spring 而不是 tween

Flutter 和 Compose 的动画 API 是镜像的:AnimatedVisibility ≈ AnimatedOpacity/AnimatedContainer,animate*AsState ≈ TweenAnimationBuilder,spring() ≈ SpringSimulation。但有个实战经验是跨框架通用的:

能用 spring 就别用 tween。tween(durationMillis = 300) 是”死时间”——不管多远都 300ms,列表项少时拖沓、多时又跟不上。spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow) 是物理模拟,距离远自动跑得久,天然有”果冻感”。iOS 的 spring 动画之所以看着高级,就是这个原因。

我在 Flutter 电商项目里做商品卡片入场动画,最早用 CurvedAnimation(Curves.easeOut),产品经理说”有点肉”;换成 spring 后他说”对了,就是这个感觉”。Compose 的 animateContentSize、AnimatedVisibility 默认都是 spring 曲线——Google 的动效团队早就替你选好了,跟着默认走一般不会错。

唯一要注意的是无障碍:remember { Animatable } 的无限循环动画(如呼吸灯),要在系统开启动画减弱(Reduce Motion)时停掉。Compose 里用 LocalAccessibilityManager 判断,Flutter 里用 MediaQuery.disableAnimations。这是上线前最容易漏的检查项。

参考:Android 官方文档《Animation in Compose》;Material 动效规范

总结

Compose 动画体系灵活强大。掌握 Transition、Animatable、InfiniteTransition 等核心 API,可实现复杂交互动效。结合性能调优与调试工具,可以确保动画在不同设备上保持顺滑体验。


分享这篇文章:


相关文章

上一篇
Android Kotlin Compose 导航与深链
下一篇
Android Kotlin Wear OS 与 Compose 多端协同