手势与弹簧动画
实现自然、基于物理效果的动效
手势与弹簧动画 是 CoddyKit 上的免费 Android Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。
交互式物理运动
最令人愉悦的动画会响应用户的手指操作,并且像真实物体一样具有惯性和弹性。要构建这类动画,您需要将手势与基于物理效果的动画结合起来。
本课将介绍 Animatable、弹簧、甩动衰减,以及如何将它们连接到拖动手势,从而构建自然、可交互的界面。
Animatable:底层控制
animate*AsState 会响应状态,而 Animatable 则为您提供命令式控制。您可以在 remember 中保存一个实例,并通过协程调用 animateTo 或 snapTo 等函数来驱动它。
这是构建手势驱动运动效果的基础。
@Composable
fun BounceOnClick() {
val scale = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
Box(
Modifier
.graphicsLayer { scaleX = scale.value; scaleY = scale.value }
.clickable {
scope.launch {
scale.animateTo(1.3f, spring())
scale.animateTo(1f, spring())
}
}
)
}弹簧:dampingRatio 与 stiffness
spring 规格由两个值定义:
- dampingRatio - 弹性程度。数值越低,回弹越明显;
1f(临界阻尼)不会超调。 - stiffness - 变化速度。数值越高,到达目标越快。
Compose 提供了 Spring.DampingRatioMediumBouncy 和 Spring.StiffnessLow 等常量,因此您很少需要直接使用原始数值。
val spec = spring<Float>(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium
)
// later, inside a coroutine:
// offset.animateTo(targetValue = 0f, animationSpec = spec)检测拖动手势
要让移动具有交互性,您需要读取触摸输入。Modifier.pointerInput 配合 detectDragGestures,会在用户移动手指时报告拖动增量。
您可以将这些增量累加到偏移量中,然后将其应用到可组合项上。
@Composable
fun Draggable() {
var offsetX by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.size(80.dp)
.background(Color.Red)
.pointerInput(Unit) {
detectDragGestures { change, drag ->
change.consume()
offsetX += drag.x
}
}
)
}使用弹簧回弹
将拖动与 Animatable 结合,即可实现“回弹”效果。拖动时,使用 snapTo 让元素立即移动到手指位置;松手后,使用弹簧执行 animateTo(0f),让元素弹回原位。
@Composable
fun SnapBack() {
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
Box(
Modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.size(80.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, drag ->
change.consume()
scope.launch { offsetX.snapTo(offsetX.value + drag.x) }
},
onDragEnd = {
scope.launch { offsetX.animateTo(0f, spring()) }
}
)
}
)
}带速度的惯性滑动
真实的惯性滑动会在手指抬起后继续移动。使用 VelocityTracker 跟踪速度,然后将其传给 animateDecay,让元素通过基于摩擦力的 DecayAnimationSpec 滑行并自然停止。
val decay = rememberSplineBasedDecay<Float>()
// on drag end, with measured velocity:
// scope.launch {
// offsetX.animateDecay(
// initialVelocity = velocity,
// animationSpec = decay
// )
// }使用锚点实现滑动关闭
对于具有固定停靠位置的滑动手势(例如滑动关闭),anchoredDraggable 会吸附到定义好的锚点。您声明各个状态及其像素位置,Compose 会处理状态之间的弹簧动画。
enum class DragValue { Start, End }
val state = remember {
AnchoredDraggableState(
initialValue = DragValue.Start,
anchors = DraggableAnchors {
DragValue.Start at 0f
DragValue.End at 300f
}
)
}无限循环动画
对于跳动的圆点或加载闪烁等连续运动,请将 rememberInfiniteTransition 与 infiniteRepeatable 结合使用。将 RepeatMode 设置为 Reverse 可实现往返播放,设置为 Restart 则会循环重播。
@Composable
fun Pulse() {
val transition = rememberInfiniteTransition(label = "pulse")
val scale by transition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(700),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
Box(Modifier.graphicsLayer { scaleX = scale; scaleY = scale })
}结合手势与弹簧动画
这是一种典型的交互模式:拖动以移动,松手后停稳。在拖动过程中跟踪速度,然后根据距离或速度,使用 animateDecay 执行惯性滑动,或者使用 spring 向锚点停靠。
底部抽屉、轮播图和可关闭卡片之所以感觉自然,正是采用了这种方式。
// pattern:
// 1) onDrag -> animatable.snapTo(current + delta)
// 2) onDragEnd -> if (fast) animatable.animateDecay(velocity, decay)
// else animatable.animateTo(nearestAnchor, spring())纯 Kotlin 中的弹簧停稳
要直观感受弹簧如何停稳,下面是一个最简的纯 Kotlin 循环。它会施加指向目标的恢复力,并在每一步衰减速度,这与 Compose 每帧执行的原理相同。
fun main() {
var pos = 0f
var vel = 0f
val target = 200f
val stiffness = 0.2f
val damping = 0.75f
repeat(8) { step ->
vel = (vel + (target - pos) * stiffness) * damping
pos += vel
println("step $step pos=${"%.1f".format(pos)}")
}
}点击、按住与多种手势
拖动并不是唯一的手势。pointerInput 还提供了用于单击、双击和长按的 detectTapGestures,以及用于捏合缩放和旋转的 detectTransformGestures。
这里会在长按时触发弹簧缩放,将手势检测与 Animatable 结合起来。
@Composable
fun LongPressScale() {
val scale = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
Box(
Modifier
.graphicsLayer { scaleX = scale.value; scaleY = scale.value }
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
scope.launch { scale.animateTo(1.4f, spring()) }
},
onPress = {
tryAwaitRelease()
scope.launch { scale.animateTo(1f, spring()) }
}
)
}
)
}快速检查
您正在构建一个可拖动卡片,要求它在松手后弹回原始位置。哪种组合是符合 Compose 惯用方式的选择?
回顾:手势与弹簧动画
现在您已经可以构建自然的交互式运动效果:
Animatable通过animateTo/snapTo提供由协程驱动的命令式控制。spring使用dampingRatio(回弹程度)和stiffness(速度)进行调节。pointerInput+detectDragGestures可将触摸转化为运动;animateDecay可添加惯性滑动。anchoredDraggable会吸附到固定位置,适合滑动交互。rememberInfiniteTransition可驱动跳动和闪烁等循环效果。
至此,Jetpack Compose 动画部分就完成了。现在您可以自信地为数值、可见性、内容和手势添加动画。
常见问题解答
「手势与弹簧动画」课时是免费的吗?
是的 — 「手势与弹簧动画」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。
「手势与弹簧动画」这节课中我会学到什么?
实现自然、基于物理效果的动效 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Android Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「手势与弹簧动画」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Android Academy 课中编写并运行代码吗?
能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。