/ Jetpack Compose  Android动画  Compose动画  AnimatedVisibility  Transition  Animatable  Android开发  Kotlin 

Jetpack Compose 动画系统深度实践:从入门到丝滑


封面

一、为什么选择 Compose 动画系统?

在传统 View 体系中,实现一个复杂动画往往需要 ObjectAnimator、ValueAnimator、TransitionManager 多套 API 组合使用,代码量大且难以维护。Jetpack Compose 通过统一的声明式动画 API,把"状态变化 → 动画过渡"这一过程抽象成简单的函数调用,极大降低了开发门槛。

Compose 动画系统的核心设计理念是:动画本质上是状态的插值过渡。当某个状态值发生变化时,Compose 会自动在新旧值之间进行插值,驱动重组并产生平滑动画,而无需开发者手动管理动画的生命周期。

  • 声明式:描述"目标状态"而不是"动画步骤"

  • 协程友好:基于 Kotlin 协程,天然支持挂起与取消

  • 可组合:多个动画可自由嵌套与组合

  • 高性能:默认跑在 RenderThread,不阻塞主线程

二、基础 API:animate*AsState 一行代码搞定过渡

animate*AsState 是最简单的动画 API,适合对单一值做平滑过渡。它返回一个 State<T>,Compose 在状态改变时自动驱动动画。

@Composable
fun ExpandableCard(expanded: Boolean) {
    val height by animateDpAsState(
        targetValue = if (expanded) 240.dp else 80.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "cardHeight"
    )

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(height)
    ) {
        // card content
    }
}

常用的 animate*AsState 变体包括:

  • animateFloatAsState:透明度、旋转角度等 Float 值

  • animateDpAsState:尺寸、间距等 Dp 值

  • animateColorAsState:颜色渐变

  • animateOffsetAsState:位置偏移

  • animateIntAsState:整数计数动画

对于自定义数据类型,可以使用 animateValueAsState 并提供自定义的 TwoWayConverter。

三、AnimatedVisibility:优雅处理显示/隐藏动画

AnimatedVisibility 专门用于处理组件的进场/退场动画,支持自定义进入和退出的动画效果。

@Composable
fun NotificationBanner(visible: Boolean, message: String) {
    AnimatedVisibility(
        visible = visible,
        enter = slideInVertically(
            initialOffsetY = { -it },
            animationSpec = tween(300, easing = EaseOutCubic)
        ) + fadeIn(animationSpec = tween(300)),
        exit = slideOutVertically(
            targetOffsetY = { -it },
            animationSpec = tween(200)
        ) + fadeOut(animationSpec = tween(200))
    ) {
        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = MaterialTheme.colorScheme.primaryContainer,
            tonalElevation = 4.dp
        ) {
            Text(
                text = message,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

进场动画(EnterTransition)和退场动画(ExitTransition)可以用 + 操作符组合,常用组合:

  • fadeIn() + scaleIn():缩放淡入,适合弹窗

  • slideInHorizontally() + fadeIn():侧滑淡入,适合侧边栏

  • expandVertically() + fadeIn():展开淡入,适合折叠面板

在 AnimatedVisibility 的子组件内部,可以使用 animateEnterExit 修饰符为子组件单独定制动画,实现错落有致的交错动画效果。

四、Transition API:多属性状态机动画

当需要根据同一状态枚举同步控制多个属性的动画时,Transition 是最佳选择。它把多个 animate* 调用统一管理,确保所有属性同步开始和结束。

enum class ButtonState { Idle, Pressed }

@Composable
fun AnimatedButton(onClick: () -> Unit) {
    var buttonState by remember { mutableStateOf(ButtonState.Idle) }
    val transition = updateTransition(targetState = buttonState, label = "button")

    val scale by transition.animateFloat(
        label = "scale",
        transitionSpec = { spring(stiffness = Spring.StiffnessMedium) }
    ) { state ->
        when (state) {
            ButtonState.Idle -> 1f
            ButtonState.Pressed -> 0.92f
        }
    }

    val backgroundColor by transition.animateColor(
        label = "bgColor",
        transitionSpec = { tween(150) }
    ) { state ->
        when (state) {
            ButtonState.Idle -> MaterialTheme.colorScheme.primary
            ButtonState.Pressed -> MaterialTheme.colorScheme.primaryContainer
        }
    }

    val elevation by transition.animateDp(
        label = "elevation",
        transitionSpec = { tween(150) }
    ) { state ->
        when (state) {
            ButtonState.Idle -> 6.dp
            ButtonState.Pressed -> 1.dp
        }
    }

    Button(
        onClick = onClick,
        modifier = Modifier.scale(scale),
        colors = ButtonDefaults.buttonColors(containerColor = backgroundColor),
        elevation = ButtonDefaults.buttonElevation(defaultElevation = elevation),
        interactionSource = remember { MutableInteractionSource() }
            .also { source ->
                LaunchedEffect(source) {
                    source.interactions.collect { interaction ->
                        buttonState = when (interaction) {
                            is PressInteraction.Press -> ButtonState.Pressed
                            else -> ButtonState.Idle
                        }
                    }
                }
            }
    ) {
        Text("点击我")
    }
}

Transition 的优势在于 Android Studio 的动画预览工具可以直接调试多状态动画时间线,极大提升开发效率。

五、AnimatedContent:内容切换动画

AnimatedContent 用于在不同内容之间做切换动画,比 AnimatedVisibility 更强大,支持自定义进出内容时的组合动画。

@Composable
fun CounterDisplay(count: Int) {
    AnimatedContent(
        targetState = count,
        transitionSpec = {
            // 数字增大时向上滑入,减小时向下滑入
            if (targetState > initialState) {
                slideInVertically { height -> height } + fadeIn() togetherWith
                    slideOutVertically { height -> -height } + fadeOut()
            } else {
                slideInVertically { height -> -height } + fadeIn() togetherWith
                    slideOutVertically { height -> height } + fadeOut()
            }.using(SizeTransform(clip = false))
        },
        label = "counter"
    ) { targetCount ->
        Text(
            text = "$targetCount",
            style = MaterialTheme.typography.displayLarge
        )
    }
}

实际项目中,AnimatedContent 常用于:

  • Loading → Content → Error 三态切换

  • 导航页面切换

  • Tab 内容切换

  • 数字/计数器动态变化

六、手势驱动动画:Animatable 与协程

对于需要手势精确控制的动画(如拖拽、滑动到删除),应使用底层的 Animatable API,它提供了 animateTo、snapTo、stop 等协程友好的控制方法。

@Composable
fun SwipeToDeleteItem(
    onDelete: () -> Unit,
    content: @Composable () -> Unit
) {
    val offsetX = remember { Animatable(0f) }
    val coroutineScope = rememberCoroutineScope()
    val density = LocalDensity.current
    val threshold = with(density) { 120.dp.toPx() }

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .pointerInput(Unit) {
                detectHorizontalDragGestures(
                    onDragEnd = {
                        coroutineScope.launch {
                            if (offsetX.value < -threshold) {
                                // 超过阈值,执行删除动画
                                offsetX.animateTo(
                                    targetValue = -size.width.toFloat(),
                                    animationSpec = tween(300)
                                )
                                onDelete()
                            } else {
                                // 回弹
                                offsetX.animateTo(
                                    targetValue = 0f,
                                    animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
                                )
                            }
                        }
                    },
                    onHorizontalDrag = { _, dragAmount ->
                        coroutineScope.launch {
                            val newValue = (offsetX.value + dragAmount).coerceAtMost(0f)
                            offsetX.snapTo(newValue)
                        }
                    }
                )
            }
    ) {
        // 删除背景
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(MaterialTheme.colorScheme.errorContainer),
            contentAlignment = Alignment.CenterEnd
        ) {
            Icon(
                Icons.Default.Delete,
                contentDescription = "删除",
                modifier = Modifier.padding(end = 16.dp),
                tint = MaterialTheme.colorScheme.onErrorContainer
            )
        }
        // 内容层
        Box(modifier = Modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) }) {
            content()
        }
    }
}

七、性能优化与最佳实践

Compose 动画性能优化的关键在于减少不必要的重组范围,以及尽量让动画跑在非 UI 线程。

  • 使用 graphicsLayer 修饰符:scale、alpha、rotation 等属性变化使用 graphicsLayer 可以跳过重组,直接在 RenderThread 执行,性能最佳

  • 避免在动画回调中分配对象:animationSpec 的 lambda 会频繁调用,避免在里面创建新对象

  • 合理使用 remember:Animatable、AnimationState 等对象必须用 remember 包裹,避免每次重组重新创建

  • shrinkToFit 优化列表动画:在列表 item 的 AnimatedVisibility 中使用 SizeTransform(clip = true) 避免超出边界的绘制

  • 使用 Android Studio Compose Preview 动画调试:在 Preview 中点击动画图标,可以可视化调试动画时间线

// 推荐:用 graphicsLayer 做 alpha/scale 动画
val alpha by animateFloatAsState(if (visible) 1f else 0f, label = "alpha")
Box(
    modifier = Modifier.graphicsLayer { this.alpha = alpha }
) { /* content */ }

// 不推荐:直接用 alpha 修饰符会触发重组
// Box(modifier = Modifier.alpha(alpha)) { /* content */ }

遵循这些最佳实践,能确保你的 Compose 动画在低端设备上同样流畅稳定,为用户带来一致的优质体验。

发布评论

热门评论区: