apple-motion-feel 是一套关于 Apple 平台动画手感的技能文档,面向使用 SwiftUI 的开发者,目标是让过渡动画看起来和用起来像系统原生行为,而不是靠反复试参数猜测弹簧数值。
【用途】 文档开篇指出,多数“这个动画感觉不对”的问题并非时长问题,而是四类范畴错误之一:对没有动量来源的对象使用了回弹;动画忽略了引发它的手势;动画被打断时从错误的数值重新开始;反馈在用户已经松手之后才到达。作者主张先修正范畴,再讨论具体数字。
【核心规则与参数表】 回弹应由动量“挣得”:用户甩出去的会过冲,用户点击的不过冲。文档给出阻尼与响应时间对照:点击驱动(按钮、开关、选择、按钮打开的面板)阻尼 1.0(临界阻尼)、响应 0.3–0.4 秒;动量驱动(滑动、甩动、拖拽释放)阻尼 0.8、响应 0.3–0.4 秒;移动/重定位阻尼 1.0、响应 0.4 秒;旋转阻尼 0.8、响应 0.4 秒;抽屉/面板阻尼 0.8、响应 0.3 秒。文档特别提醒:点击打开的抽屉应使用 1.0,而不是抽屉行的 0.8,因为该行数据来自拖拽打开的抽屉;应看触发方式,而非组件名称。文档还提供症状对照:点击时晃动说明阻尼过低而非时长过长;迟滞通常是响应过大;廉价、机械感通常是在该用弹簧的地方用了线性或 ease-in-out 曲线。
【SwiftUI 预设】 .smooth 为临界阻尼、无过冲,是点击驱动 UI 的默认选择;.snappy 为轻微欠阻尼、小幅过冲,适合轻快反馈和小元素;.bouncy 阻尼低、可见振荡,适合俏皮、刻意的表达。三者默认 duration 为 0.5,均接受 duration: 与 extraBounce: 参数。文档建议优先使用具名预设而非手写 .spring(response:dampingFraction:),因为预设表达意图。在现代 .spring(duration:bounce:) 形式中,bounce 是可读的轴:0.0 为临界阻尼平滑到达;0.15 轻快但不弹;0.2 适合带速度的拖拽释放;0.3 夸张、刻意;大于 0.4 属于极端,几乎不适用于 UI;小于 0 为过阻尼。建议从 bounce: 0 开始,只有动画确有动量支撑时才增加。
【手势驱动动画】 文档认为这是“高级感”真正决胜之处,也是调时长帮不上忙的地方:应在指针按下时响应而非释放时;手势过程中 1:1 跟手,不用缓动或平滑,用户本身就是动画;把释放速度交给弹簧,否则快速甩动后从零速启动的弹簧会明显顿住。文档给出归一化相对速度公式 relativeVelocity = gestureVelocity / (targetValue − currentValue),以及用指数衰减预测静止点的函数 project(initialVelocity:decelerationRate:)(默认减速率 0.998);在边界处使用橡皮筋效果,阻力随超出距离逐步增大,不要硬停。
【可中断性与一致性】 不要从目标值开始动画,应读取屏幕上当前的呈现值,否则被打断的动画会跳变;SwiftUI 弹簧默认从当前值开始动画,这正是用关键帧或计时器手写形变更糟的原因。动画期间不要锁输入,.allowsHitTesting(false) 需要给出理由。每个属性只保留一个动画来源,不要混用隐式 .animation、withAnimation 和手势驱动更新。动画上下文要放在条件块之外,写在 if 里的 .animation() 会随视图一起销毁,导致移除时没有动画。动画顺序按成本与可靠性排列:先变换(缩放、位移、旋转),再尺寸,最后身份变化;两个互斥视图之间的“形变”属于身份变化,无法插值,应对一个持续存在的视图做属性动画。一致性胜过正确性:同一交互在不同位置以不同速度动画会显得粗糙,应在设计系统中命名一次并引用。
【无障碍与验证】 每个应用内动画都需要“减弱动态效果”分支:替代而非删除,优先级为交叉淡化 → 缩短到 0.1–0.15 秒且无回弹 → 直接切换;手势跟踪保留,只收敛释放弹簧。文档提供三条 grep 命令用于查找无意图的魔数、裸 withAnimation、被困在条件里的动画上下文,并附检查清单。
【适用场景与限制】 适用于 SwiftUI 及 Apple 平台的交互动画设计评审与实现。文档本身为经验性指南,给出的数值区间是建议而非强制标准;其中关于预设语义和可中断性的说法参考了 Apple 的 WWDC23 相关场次。文档未涉及其他平台、具体项目接入步骤或工具化校验,相关配置暂未提供。