☰
FireMonkey 十种动画插值类型详解与选型指南
2026/10/1 17:08:29 网站建设 项目流程

前阵子我把一个页面切换动画从 Linear 改成 Quad,用户第一时间反馈说“速度快了不少”。实际上动画时长一秒没改,我只是换了TFloatAnimation的Interpolation属性。FireMonkey 动画的观感差异,很大程度由插值类型决定,这也是最容易被人忽略的一项设置。这篇文章就把十种插值类型逐个拆开,配上行之有效的代码和实操截图级别的说明,希望能帮你在做界面动效时不再“只调时长、不会调手感”。

1. 动画插值到底是什么

1.1 从“开始值到结束值”到“中间过程”

FireMonkey 里做动画,最常见的套路是给控件的某个属性指定起始值和终止值,然后让引擎自动完成中间的过渡。比如把一张矩形的位置从屏幕左侧移到右侧,代码里只需要写StartValue和StopValue,剩下的时间轴推进都由 FMX 负责。问题就在这里:从开始到结束的中间过程,存在无数种走法,可以是匀速直线到达,也可以开头慢、结尾快,还可以先冲过头再拉回来。

插值类型(Interpolation Type)决定的就是这条中间路径的形状。FMX 引擎会把动画时间归一化成 0 到 1 之间的数值,0 代表动画刚开始,1 代表动画结束;插值函数则把这个时间比例映射成进度比例。如果映射关系是直线,就是线性插值;如果映射关系是曲线,那就会出现加速、减速、反弹、震荡等不同视觉结果。

用生活化的例子理解:你从一个路口走到另一个路口,匀速走是一回事,先慢悠悠起步、后半程狂奔是另一回事,先往后退两步再冲刺又是完全不同的感觉。三者最终都到达同一个路口,但路上的节奏完全不一样。FireMonkey 里的TInterpolationType就是专门用来控制这个“走路节奏”的东西。

1.2 FMX 的动画模型与归一化时间

在 FireMonkey 中,所有动画都继承自TAnimation,比如我们最常用的TFloatAnimation、TColorAnimation、TBitmapAnimation。它们内部计算时会把动画时长Duration切成一段一段,并在每一帧根据当前时间计算出NormalizedTime。NormalizedTime是一个 0 到 1 之间的值,0 表示起点,1 表示终点。

然后 FMX 会把NormalizedTime交给当前生效的插值器。默认的插值器是TLinearInterpolator,它会原封不动地返回这个时间比例,所以进度和时间严格同步。而其他插值器会返回一个不同于输入时间的值,有的返回比输入时间更小的数,导致动画前期推进缓慢;有的返回比输入时间更大的数,导致动画前期就冲出去一截。理解了这一层,再去看每种插值类型的曲线图时你就能知道,曲线越高,代表同一时间点上推进得越多。

我刚开始学 FireMonkey 时,总把“动画时长”等同于“动画快慢”。后来才发现,时长决定的是总跨度,插值类型决定的是跨度的分布。同样的两秒动画,用 Exponential 和用 Linear 的肉眼观感,几乎像是两个不同时长的动画,这就是插值类型的魅力所在。

2. FireMonkey 动画的最小实现框架

2.1 一个可以直接跑的 FloatAnimation 示例

学习插值类型之前,先搭一个最简动画架子。下面这段代码让一个名为Rectangle1的矩形在水平方向移动,这是我在测试插值时最常用的最小示例:

procedure TForm1.DoMoveRectangle; var Anim: TFloatAnimation; begin Anim := TFloatAnimation.Create(Rectangle1); Anim.Parent := Rectangle1; // 动画对象挂到控件上 Anim.PropertyName := 'Position.X'; // 要属性动画的字段 Anim.StartValue := Rectangle1.Position.X; Anim.StopValue := ClientWidth - Rectangle1.Width - 20; Anim.Duration := 1.0; // 单位:秒 Anim.Interpolation := TInterpolationType.Linear; Anim.Start; end;

这是一个很标准的TFloatAnimation用法。注意Parent和PropertyName是 FireMonkey 动画的两个关键设置:Parent决定动画接收者是谁,PropertyName决定要改变哪个属性。这里改变的是Position.X,所以StartValue和StopValue都是数值型。如果换成RotationAngle,那动画就会变成旋转。

很多新手会忘记设置Parent,结果动画对象虽然创建了,却不会驱动任何控件,界面一片寂静。另外,动画默认不会在结束后自动释放,你可以在OnFinish事件里调用Free,或者像我这样在页面关闭时统一释放。

2.2 把插值类型接到动画上

把上面代码里的Interpolation从Linear改成其他枚举值,比如Quad、Cubic、Bounce,就能直观感受到动画节奏的变化。如果你想在运行时切换插值类型,最简单的方式是配合下拉框:

procedure TForm1.ComboBox1Change(Sender: TObject); begin if Assigned(CurrentAnim) then CurrentAnim.Interpolation := TInterpolationType(ComboBox1.ItemIndex); end;

下拉框的 ItemIndex 和枚举值对应关系需要注意。如果不想依赖枚举顺序,可以在初始化时按枚举顺序填列表:

for var LType := Low(TInterpolationType) to High(TInterpolationType) do ComboBox1.Items.Add(GetEnumName(TypeInfo(TInterpolationType), Ord(LType)));

这里用到了System.TypInfo的GetEnumName,它可以保证下拉项和枚举值严格一一对应,省去手工维护列表的麻烦。运行时改插值的好处是,你能在同一个界面上反复对比十种类型,不需要每次都重新编译,这也是后面做交互调试台的基础。

3. 十种动画插值类型图解与实测

3.1 Linear 和 Quad:从匀速到匀加速

我先从最常用的两类说起。

Linear 是最直观的插值,时间走到 50%,进度也刚好是 50%。它没有任何加速或减速,适合进度条、滚动提示这类需要稳定节奏的场景。缺点也很明显,如果用在弹窗动画上,开头的移动会比较机械,缺少柔和感。

Quad 则是一条二次曲线,同一时间点上,前期推进量小于时间比例,后期推进量大于时间比例。举个例子,动画进行到 50% 时,Linear 已经走了一半路,Quad 可能只走了四分之一左右,所以你会觉得它“起步慢、后段猛”。这种节奏非常适合卡片入场、按钮位移等需要一点“冲劲”的场景。如果目标是从底部滑出的面板,用 Quad 会比 Linear 舒服得多。

我在实际项目中,移动端列表项入场动画常选 Quad。原因很简单:列表项大多从屏幕边缘滑入,用户注意力在开头,速度慢一点能让人看清内容,等后半段速度上来,又能营造流畅的视觉感受。这个“先慢后快”的节奏特别适合营造“被推入画面”的感觉。

3.2 Cubic、Quart、Quint:加速感一级比一级强

Cubic 是三次曲线,加速感比 Quad 更强烈。在移动端转场动画里,Cubic 是很多设计师喜欢的效果:开始几乎不动,中间开始明显提速,结尾干脆利落。用文字曲线示意,大概长这样:

Linear : ▁▂▃▄▅▆▇█ Quad : ▁▂▃▄▅▆▇█ Cubic : ▁▂▃▄▅▆▇█ Quart : ▁▂▃▄▅▆▇█ Quint : ▁▂▃▄▅▆▇█

严格来说,上面图示只是示意方向,实际曲线的弯曲程度是递增的。Quart 是四次曲线,Quint 是五次曲线,后者在动画前半段几乎处于“原地踏步”状态,然后在最后三分之一时间里快速冲到终点。这种极端加速感不常用,但用在数字跳动、倒计时结束、成就解锁这类强调“最后一刻爆发”的场景时,效果非常惊艳。

需要提醒的是,Quart 和 Quint 千万别和长时长的动画混用。如果动画超过三秒,前几秒几乎看不到移动,用户可能会以为程序卡死了。我踩过这个坑,曾经把一个成就动画配成 Quint,前两秒完全没有反馈,结果用户反复点按钮,最后来了个瞬移感极强的加速,体验很糟糕。后来我把它和时间控制在 800 毫秒以内,才找到合适的节奏。

3.3 Sine 和 Exponential:丝滑过渡与爆发冲刺

Sine 的类型会先加速后减速,像一条 S 形曲线。它的特点是开头不会太慢,结尾也不会突然刹死,过渡非常平滑。这种插值在页面淡入淡出、图标平滑移动、进度圆环等场景中非常通用,可以说是“万金油”。很多 UI 框架里的 ease-in-out 设计本质就是接近 Sine 的效果。如果你不知道选什么插值类型,选 Sine 通常不会出错。

Exponential 则是指数曲线,它的视觉特征是前期几乎停滞、中期突然加速、后期极快收尾。这个性质让它很适合做“从 0 开始冲刺到满值”的进度效果,比如下载完成、加载完成、技能释放。用数据举例,动画进行到 50% 时,Linear 推进了 50%,Exponential 可能才推进不到 10%,但到 70% 时间点时可能已经推进到 60%,最后 10% 时间则完成剩余冲刺。

使用 Exponential 要特别留意终点附近的边界感。因为它结尾极快,如果用户需要看清目标位置的落点,很容易错过。我在做一个充值成功动画时,数字翻转用了 Exponential,金币图标飞向余额区域时用了它,但最后落点的“弹跳反馈”是用另一个 Bounce 动画补上的,不然只看指数冲刺会觉得太突然。

3.4 Back、Bounce、Elastic:回拉、反弹与弹性震荡

这三类都有明显的“超出目标”行为,适合做有个性的提示动画。

Back 的视觉效果是“先反向运动一段距离,再朝目标方向前进”。想象拉弓射箭,箭会先往后拉一点,再飞速射出去。用在卡片弹出上,Back 会让卡片先从中心稍微缩小或从原始位置稍微移动,再弹到最终位置,整个过程有一种“蓄力感”。实际使用时注意反向位移不能太大,否则用户会产生“点错了”的错觉。

Bounce 模拟的是物体落地后的反弹。动画进行到接近终点时,进度会先在终点附近上下波动,再稳定下来。这种插值做“重量感”特别好,比如按钮掉落到面板上、消息气泡落地、角色跳跃落地后的回弹。Bounce 的反弹次数和幅度是内置设计好的,通常第一下反弹最明显,后续越来越小。

Elastic 更像弹簧或琴弦震荡,它会围绕目标值来回振荡很多次,直到逐渐停下来。这个效果视觉冲击力最强,适合红包弹出、勋章解锁、节日撒花之类的场景。但也是最容易做过头的一个,振荡次数多、振幅大时,页面会显得非常杂乱,甚至让用户觉得头晕。我在项目里只用它做一次性提醒动画,并且严格控制振幅不超过目标值的 8%。

这三者还有一个共同坑点:它们都会有“短暂越界”的过程。对位置类动画来说,越界会让控件跑到目标位置之外,如果父容器有裁剪ClipChildren,动画边缘会被切断;对透明度动画来说,越界会溢出到大于 1 或小于 0 的数值,FMX 虽然会做钳制,但你该知道这是正常现象,不是控件坏了。

3.5 一张表总结十种类型的选型参考

插值类型视觉感受适合场景不宜场景
Linear匀速、机械进度条、滚动指示弹窗、强调动画
Quad先慢后快卡片入场、面板滑入长时间等待动画
Cubic加速感明显页面转场、强调位移实时反馈型按钮
Quart强烈加速数字冲刺、成就解锁超过 3 秒的长动画
Quint极端加速宣传片级别的终点击打日常 UI 动效
Sine平滑加速再减速淡入淡出、常规位移需要突然爆发的感觉
Exponential前段极慢后段爆发加载完成、技能释放需要持续反馈的场景
Back先反向再正向卡片弹入、图标激活频繁切换的页面
Bounce落地反弹重量提示、消息掉落精密数据界面
Elastic弹性震荡红包弹窗、节日特效高频滚动列表

这张表不是标准答案,但可以帮你少走弯路。实际项目里,我经常拿 Sine 做默认,用 Quad 做入场,用 Bounce 做落地反馈,用 Elastic 做一次性高亮提示。这个组合在大多数移动端和桌面端界面里都足够体面。

4. 实操:做一个运行时插值试玩台

4.1 界面布局思路

纸上谈兵没意思,最好的学习方式是在一个界面里同时跑十种插值。我建议你新建一个 FMX 项目,在窗口上放一块TRectangle作为移动目标,再放一个TComboBox用于选择插值类型,一个TButton负责触发动画,再加一条TLabel显示当前动画进度。布局并不复杂,关键是三个控件之间的数据流要打通。

下拉框初始化可以写成这样:

procedure TForm1.FormCreate(Sender: TObject); var LType: TInterpolationType; begin for LType := Low(TInterpolationType) to High(TInterpolationType) do ComboBox1.Items.Add(GetEnumName(TypeInfo(TInterpolationType), Ord(LType))); ComboBox1.ItemIndex := 0; end;

这样列表框会列出十种插值类型,并且顺序和枚举值严格对应。运行时选到哪一项,动画就使用哪一种插值。

4.2 核心切换代码与事件绑定

触发动画的按钮里,我每次会先清掉旧的动画,避免上一次动画没结束后又叠加一个新动画,导致控件位置错乱。然后创建新的TFloatAnimation,把插值类型直接赋给动画对象:

procedure TForm1.btnRunClick(Sender: TObject); var i: Integer; Anim: TFloatAnimation; begin for i := Rectangle1.Animations.Count - 1 downto 0 do Rectangle1.Animations[i].Stop; Anim := TFloatAnimation.Create(Rectangle1); Anim.Parent := Rectangle1; Anim.PropertyName := 'Position.X'; Anim.StartValue := 20; Anim.StopValue := ClientWidth - Rectangle1.Width - 20; Anim.Duration := 2.0; Anim.Interpolation := TInterpolationType(ComboBox1.ItemIndex); Anim.OnProcess := TrackProgress; Anim.AutoReverse := False; Anim.Start; end;

OnProcess事件会在动画执行过程中持续触发,我们可以用它实时更新屏幕上的进度文字,这样就能清楚看到每种插值在不同时间点的推进情况:

procedure TForm1.TrackProgress(const Sender: TObject); var LAnim: TFloatAnimation; begin LAnim := Sender as TFloatAnimation; Label1.Text := Format('插值类型: %s 时间: %.2f 进度: %.0f%%', [ GetEnumName(TypeInfo(TInterpolationType), Ord(LAnim.Interpolation)), LAnim.NormalizedTime, LAnim.NormalizedTime * 100 ]); end;

这个“试玩台”跑起来后,你可以挨个切换下拉项,观察矩形在屏幕上从左向右的运动轨迹。你会明显看到 Linear 是匀速的,Quad 前期基本不动,Bounce 在终点附近反复弹跳,Elastic 像一条松开的橡皮筋一样晃来晃去。这种归纳式对比,比查阅任何文档都印象深刻。

4.3 如何进一步观察曲线细节

如果你想把曲线看得更直观,可以把矩形换成一个TPath或TLine,并在OnProcess中把NormalizedTime和当前进度投射到二维坐标上,逐帧画点,形成一条实时曲线。更简单的办法是让矩形做纵向移动,然后再横向移动,两轴分别接入不同插值,组合起来你就能看到弧线路径。

做这种调试台的时候,我强烈建议把Duration故意设长一点,比如 2 到 3 秒。因为插值类型之间的差异在低速下才容易看清,设置太短会眨眨眼就结束了,根本分不出 Exponential 和 Quint 的区别。

还有一个实用技巧:把AutoReverse打开,你可以看到动画从起点到终点再到起点的完整往返过程。部分插值类型(比如 Bounce、Elastic)在反向过程中会和正向过程完全不一样,这也会提醒你,入场动画和退场动画不应该直接暴力反转。有些效果只适合“进出”其中一个方向。

5. 常见问题与排查技巧

5.1 几个典型坑和处理方法

问题原因解决方式
动画不生效没有设置 Parent将动画的 Parent 设为要驱动的控件
属性值一直跳动动画对象叠加创建新动画前停止旧的动画
Quad/Bounce 在布局中被裁剪父容器裁剪了越界内容调整父容器尺寸或关闭 ClipChildren
Elastic 看起来乱掉振幅过大或时长过长降低动画幅度、缩短到 1 秒内
切换插值类型后没变化动画已经运行中停止并重新创建动画再设置
色值动画出现奇怪颜色使用 ColorAnimation 时插值不当优先用 Linear 或 Sine

最容易被忽视的是第一个:Parent。FireMonkey 的动画不像传统 Timer 回调那样直接操作控件,而是寄宿在控件树里,每帧由全局动画管理器统一驱动。如果你没设置 Parent,动画对象就是游离的,什么都不会发生。创建动画时最好传入控件的实例作为 Owner,并且把 Parent 赋给控件,这是双保险。

叠加问题也经常遇到。当一个动画还在运行,用户再次点击按钮触发新动画时,两个动画同时改变同一个属性,属性值会被后来居上的动画覆盖。表现就是控件卡在奇怪位置,甚至来回抖动。解决办法是:每次创建前先遍历Rectangle1.Animations,把已经存在的动画Stop掉。这个习惯我在所有 FMX 动效项目里都保持。

5.2 与时长、延迟和重复的组合经验

插值类型不是孤立工作的,它经常和Duration、Delay、AutoReverse、RepeatCount一起配合。我总结出的经验是:插值曲线越“激烈”,动画时长越要短。Elastic 通常控制在 600 到 1000 毫秒,Bounce 控制在 800 到 1400 毫秒,Back 控制在 300 到 600 毫秒。如果时长超过两秒,再美的曲线也会显得拖沓,尤其是带越界效果的插值,会让用户觉得系统不稳定。

延迟启动也很关键。页面初始化时,多个元素要依次入场,我常给第二个、第三个元素设置Delay := 0.1 * Index,让入场动画像波浪一样逐层出现。插值类型选择 Sine 或 Quad 会比较合适,因为延迟序列本身已经营造了节奏感,再用强烈的 Elastic 反而会互相干扰。

重复播放时要注意,RepeatCount配合 Bounce 或 Elastic 会产生连续弹跳效果,很容易让界面显得焦虑。我只建议在“等待连接”“加载中”这类持续反馈场景使用 Linear 或 Sine 的循环动画,并且重复次数不要无限,通常 3 到 5 次再停止比较自然。

5.3 给初次接触插值的开发者的上手指引

如果你是第一次接触 FireMonkey 动画插值,不建议一口气把十种类型全部背下来。先掌握三个就足够覆盖日常 80% 的需求:Linear、Quad、Sine。Linear 让你理解动画驱动的基本逻辑,Quad 帮你体会“加速感”,Sine 让你知道“平滑过渡”是什么感觉。剩下七个里,你只需要在做个性和强调效果的场景中再回头查。

上手路径可以这样安排:先跑通最小示例,把矩形的位置动画跑起来;再换成 Quad,观察前后段速度差;然后把Duration从 2 秒改成 0.5 秒,感受加速度被压缩后的变化;最后加入 Bounce 和 Elastic,用“过头再回来”的效果强化印象。整个流程用一个下午就能完成,投入产出比非常高。

在真实业务中,动画插值不是越快越好,也不是越花哨越好。它更像食材里的调味品,合适最重要。界面动效的目标是让用户感到自然、清晰、顺畅,而不是用花哨的弹跳秀技术。

我在实际项目里的习惯是,把一张插值速查表放在团队共用文档里,每个动画都要写明“为什么选这个插值类型”。这样做了一年多之后,团队成员写动效的效率明显提升,因为大家在选型时不再凭感觉,而是先问自己想表达什么情绪:是想让元素“自己滑出来”,还是“被推出来”,还是“从天上掉下来”?

下午调试那个试玩台时,我发现同一段 500 毫秒的动画,用 Quint 时矩形前半程几乎纹丝不动,正当我怀疑代码写错,它在最后 150 毫秒里突然冲出去,那一瞬间把旁边同事都吓了一跳。那次之后我彻底明白,文档里写“插值类型决定动画节奏”这句话实在太轻描淡写了,它决定的是用户对界面性格的感知。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询