0Pricing
Android Academy · 课时

手势与弹簧动画

实现自然、基于物理效果的动效

手势与弹簧动画 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 动画为何重要
  2. animate*AsState 基础
  3. AnimatedVisibility 与过渡
  4. 手势与弹簧动画
← 返回 Android Academy