实战案例3:社交应用的动画设计
聊到社交应用的动画,我脑子里第一个蹦出来的就是点赞动画。说实话,这个小小的交互,在 Compose 里做起来比 View 系统优雅太多了。今天我就拿三个最典型的场景——点赞动画、消息弹窗、列表刷新——来拆解一下,看看 Compose 动画到底怎么落地。
一、点赞动画:从“硬跳”到“弹性反馈”
先说说点赞。我记得在 View 时代,很多团队直接用帧动画或者 Lottie 来搞。但 Compose 里,我们完全可以用 animateFloatAsState 配合 spring 效果,做出那种“按下去弹回来”的质感。
核心思路其实就两步:
- 用
Animatable控制缩放比例 - 用
animateColorAsState控制颜色变化
来看代码:
@Composable
fun LikeButton(
isLiked: Boolean,
onToggle: () -> Unit
) {
val scale = remember { Animatable(1f) }
val color by animateColorAsState(
targetValue = if (isLiked) Color.Red else Color.Gray,
animationSpec = tween(300)
)
LaunchedEffect(isLiked) {
if (isLiked) {
// 先弹大一点,再回弹
scale.animateTo(1.3f, spring(dampingRatio = 0.3f))
scale.animateTo(1f, spring(dampingRatio = 0.5f))
}
}
IconButton(onClick = {
onToggle()
// 触发动画
}) {
Icon(
imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.Favorite,
contentDescription = "点赞",
modifier = Modifier.graphicsLayer(scaleX = scale.value, scaleY = scale.value),
tint = color
)
}
}
这里有个小坑——spring 的 dampingRatio 参数。我刚开始调的时候,直接用了默认值 1,结果动画特别“硬”,没有那种 QQ 弹弹的感觉。后来改成 0.3 左右,效果就对了。你想想看,点赞这种操作,用户要的就是那种“按下去有反馈”的爽感。
Canvas 配合 Animatable 做粒子扩散。不过一般场景下,缩放+颜色变化已经够用了。
二、消息弹窗:从底部滑入,带遮罩
消息弹窗在社交应用里太常见了。我个人习惯用 AnimatedVisibility 配合 slideInVertically 来做。但要注意一点——弹窗出现时,背景遮罩的透明度也要跟着动画,不然会显得很突兀。
我踩过的一个坑是:遮罩和弹窗的动画时长不一致,导致遮罩先消失,弹窗还挂在屏幕上,特别丑。解决方案很简单,用同一个 AnimationState 来控制:
@Composable
fun MessagePopup(
visible: Boolean,
content: @Composable () -> Unit
) {
val transition = updateTransition(targetState = visible, label = "popup")
val alpha by transition.animateFloat(label = "alpha") { state ->
if (state) 0.6f else 0f
}
val offsetY by transition.animateDp(label = "offset") { state ->
if (state) 0.dp else 200.dp
}
if (visible || transition.currentState != transition.targetState) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = alpha))
.clickable { /* 点击遮罩关闭 */ }
) {
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = offsetY)
.background(Color.White, RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
.padding(16.dp)
) {
content()
}
}
}
}
为什么用 updateTransition 而不是 AnimatedVisibility?因为 updateTransition 可以让你同时控制多个属性,而且动画状态是同步的。说白了,就是遮罩和弹窗的“步调”完全一致。
三、列表刷新:下拉刷新动画的“丝滑”体验
列表刷新这个场景,我见过太多人直接用系统默认的 SwipeRefresh。但说实话,那个动画效果太“死板”了。在 Compose 里,我们可以用 pullRefresh 配合自定义指示器,做出更自然的反馈。
核心逻辑是:
- 监听下拉偏移量
- 根据偏移量控制指示器的旋转角度和透明度
- 达到阈值后触发刷新,并显示加载动画
来看一个简化版的自定义指示器:
@Composable
fun CustomRefreshIndicator(
progress: Float, // 0f ~ 1f
isRefreshing: Boolean
) {
val rotation by animateFloatAsState(
targetValue = if (isRefreshing) 360f else progress * 360f,
animationSpec = if (isRefreshing) infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Restart
) else tween(300)
)
Canvas(modifier = Modifier.size(40.dp)) {
val strokeWidth = 4.dp.toPx()
val radius = (size.width - strokeWidth) / 2
// 画弧线
drawArc(
color = Color.Blue,
startAngle = rotation,
sweepAngle = progress * 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
}
}
嗯,这里要注意一点:infiniteRepeatable 在 Compose 里是专门用来做循环动画的。我刚开始用的时候,忘了加 repeatMode,结果动画只播了一次就停了,排查了半天才发现。
nestedScroll 来处理冲突。我曾经在一个项目里没处理好,导致下拉和列表滚动互相“打架”,用户体验极差。
知识体系总览
我把这三个场景的核心知识点整理成了一张图,方便你对照理解:
总结一下
这三个场景其实都指向同一个核心:动画的本质是状态变化的视觉反馈。点赞是瞬时反馈,弹窗是阶段性反馈,列表刷新是持续性反馈。在 Compose 里,你只需要关注状态本身,动画的“表演”交给框架就好。
我个人觉得,Compose 动画最大的优势就是“声明式”——你告诉它“我要什么效果”,它帮你算好中间帧。这在 View 系统里,你得写一大堆 ValueAnimator 和 AnimatorSet,想想就头疼。
- 点赞用
Animatable+spring做弹性反馈 - 弹窗用
updateTransition同步遮罩和内容动画 - 列表刷新用
pullRefresh+ 自定义指示器 - 所有动画都要考虑状态同步,避免“各跳各的”
好了,这三个案例够你消化一阵子了。下次遇到类似场景,直接套用这个思路就行。