做Delphi开发这么多年,如果要让我挑一个最能提升界面质感、投入产出比又极高的功能,FireMonkey的动画系统绝对排在前三。几乎每个第一次接触FMX窗体的人,都会被那种丝滑的过渡效果吸引:窗体淡入淡出、面板从侧边滑入、弹窗带一点柔和的缩放……而这些效果并不需要引入第三方控件,也不用自己去写复杂的定时器或插值算法,FireMonkey内置的动画组件就能直接搞定。尤其是配合Delphi 12里更完善的源码和跨平台能力,用上动画组件之后,窗体的动态效果能很自然地融入业务场景,比如登录窗淡入、设置面板滑出、消息提示缩放弹出。
这篇文章就是基于“一步一步学习使用FireMonkey动画,使用动画组件为窗体添加动态效果”这个核心需求来写的。适合谁看?刚接触FMX的Delphi开发者,或者已经在用VCL、想给窗体增加现代感动态效果的同行。我用实际做过的几个窗体效果作为例子,从原理到参数再到完整操作步骤一起拆开讲,保证看完就能在自己项目里落地。
先说一个我自己常有的感受:很多人以为动画是“锦上添花”的装饰,但真正用好了,它其实是界面交互的一部分,能告诉用户“发生了什么变化”。比如页面切换时,新内容滑入的方向能暗示层级关系;弹窗淡入淡出,能区分模态和非模态操作。FMX的动画组件最大的好处是“属性驱动”——不需要手动管理帧循环,只要指定起始值、结束值和时间,框架自动帮你做中间插值。这种思路非常直观,也容易出效果。
接下来我从原理、参数、实操到排查,把整个FMX动画组件的使用路径完整过一遍。
1. 先搞清楚FireMonkey动画的底层逻辑再动手
1.1 属性动画的本质:它驱动的是任意对象的任意属性
FireMonkey的动画系统和我最早接触的VCL完全不同。VCL时代想做动画,基本是Timer + 手动改属性,每触发一次Tick改一点,非常繁琐而且容易闪烁。FMX的做法是属性动画(Property Animation):你告诉它“从A变到B,时间多久”,系统自动计算中间帧并刷新界面。
这意味着什么呢?在FMX里,几乎可见控件都继承自TFmxObject家族,而这些控件暴露出的数值型属性(Opacity、Position.X、Scale.X、RotationAngle、Margins.Top等等)都可以作为动画驱动目标。换句话说,只要是控件上能找到的数值属性,就可以做动画。这正是“为窗体添加动态效果”最核心的底层支持:窗体本身也是一个控件,所以窗体的透明度、位置、大小、缩放都能被动画组件接管。
实际还有更灵活的用法:动画组件不只是绑定控件,也可以绑定一些非可见对象的属性。比如绑定一个TGradient的Color属性,就能让背景颜色平滑过渡。TFloatAnimation支持几乎所有Single类型的属性,而TColorAnimation专门处理TAlphaColor类型的颜色值,TAnimator类还提供了一组静态方法,不需要在设计器里拖组件,就能直接用代码控制动画。
1.2 四大内置动画组件怎么分工
FMX内置了几种动画组件,常用的就四个,把它们的分工理清楚,选型就不会错。
TFloatAnimation最常用,负责所有浮点数值属性的动画,包括Opacity透明度、Position.X/Y、Scale.X/Y、RotationAngle、宽度高度等。这个组件是绝大多数窗体动态效果的主力。
TColorAnimation负责颜色过渡,例如窗体背景色从浅蓝渐变到白色、跑马灯式的边框变色、提示信息的背景闪烁等都靠它。注意它针对TAlphaColor类型,不是TColor。
TGradientAnimation专门处理渐变对象,适合做背景渐变的动态切换,比如登录页背景从深色渐变为浅色。
TPathAnimation可以让控件沿着一条路径移动,适合更复杂的轨迹动画,比如弹窗从屏幕角落飞入。
除了这几个组件,还有一个经常被忽略的TAnimator类。它不是组件,是一个静态工具类,提供AnimateFloat、AnimateColor等静态方法。用代码临时做一个动画时非常方便,不需要在设计器里放组件。后面我会在实操环节里给出它的代码示例。
1.3 动画组件的触发方式:图省事还是图可控
FMX动画组件的运行机制很灵活,主要由Trigger、TriggerTarget、Enabled和事件来协调。这里有一个常见的误区:新手往往以为把动画组件放进窗体,它就会自动播放,结果发现运行时毫无反应。实际上,动画组件分为“手动触发”和“自动触发”两类工作方式。
自动触发靠Trigger属性,取值是字符串,例如IsVisible、IsFocused、IsMouseOver等。系统会在TriggerTarget指定的对象上检测这些状态变化,一旦满足条件就自动Start动画。例如你想让Panel在鼠标悬停时放大,可以在动画组件上设置Trigger = IsMouseOver,TriggerTarget设为该Panel,这会省去写事件处理。但这种方式的缺点是你不一定能精准控制动画的播放方向和时机。
手动触发则更可控,通常使用Start、Stop、StartValue等运行时方法。比如点击按钮后,让子窗体滑入或淡入,这时可以完全控制开始时间和结束值。我的建议是:登录窗这类固定流程可以用Trigger自动播放,业务里需要与用户操作紧密配合的动画,一律手动触发。因为自动触发经常会在你不想要的时候跳舞,比如窗体启动时多个Trigger同时命中,动画互相打架。
2. 核心参数逐个拆解:把动画调到“刚刚好”
2.1 Duration、StartValue和StopValue:先算再填
这三个参数是每个动画组件都有的。Duration单位是秒,决定整个动画持续多长时间。StartValue是动画开始时的属性值,StopValue是结束时的值。它们看似简单,但经验往往体现在数值怎么定。
透明度动画:窗体从完全透明到不透明,StartValue = 0,StopValue = 1,Duration = 0.4。这组值在我做过的项目里几乎通用,0.4秒既不会觉得拖沓,又足够柔和。如果想做出“呼吸感”,把Duration放宽到1.5左右,配合AutoReverse就有循环效果。
位置动画:Panel从屏幕右侧滑入,StartValue为Panel的X坐标初始位置,StopValue为最终位置。这里有一个容易被忽略的细节:如果你在设计器里放置的是非Layout对齐的控件,动画组件内部是直接修改Position。但如果控件的Align属性是Right或Left,直接改Position往往无效,因为对齐布局系统会强制覆盖坐标。后面排查章节我会再详细展开这个问题。
缩放动画:以窗体的Scale.X/Y为属性,从0.8到1.0。注意FMX的Scale变换默认围绕控件的左上角,而不是中心点。想让弹窗从中心缩放弹出,必须先把RotationCenter或ScaleCenter设为0.5, 0.5。这一点初学者几乎必踩,后面我给出具体操作。
时间也值得琢磨场景差异。窗体淡入0.3秒合适,通知气泡类可以从0.15秒起,而抽屉式页面滑入用0.5秒会有高级感。最快的方式是先按经验值跑一遍,再微调Duration。
2.2 Interpolation插值模式:决定动画手感的关键
同一组StartValue和StopValue,配上不同的插值模式,最终动画效果可能天差地别。FireMonkey的Interpolation属性提供了Linear、Sinusoidal、Quadratic、Cubic、Exponential、Elastic等模式。
Linear是线性插值,速度恒定,适合颜色变化或进度条这类需要均匀变化的场景。但我个人很少用它做位移和缩放,因为机械感太强——真实世界里的运动都是有加速和减速的。
Sinusoidal是正弦插值,速度是先快后慢,近似一个“由动到静”的缓出效果。这是我最常用的模式,适合窗体淡入、弹窗缩放,给人一种自然阻尼感。
Quadratic和Cubic分别是一次和三次多项式插值,加速或减速效果更明显。如果想让Panel滑入时有明显的加速冲击感,选Quadratic;想让对象快速“甩”到终点并戛然而止,选Cubic。
Exponential是指数插值,起始非常慢然后越来越快,适合强调“离场”的动画,例如窗体退出时的快速缩小消失。
Elastic是弹性效果,会有回弹,像一个球落地后弹几下。适合趣味性场景,比如消息气泡弹出时的轻微抖动,但业务系统里尽量慎用,它会干扰用户对信息稳定性的感知。
我在做实际项目时有个习惯:给团队定了默认值——Opacity、Color类的动画用Sinusoidal;Position、Scale类的动画也先用Sinusoidal,再根据观感在Quadratic和Cubic之间切换。Linear就留给颜色相关效果。
2.3 AutoReverse、Loop与事件回调的配合
动画组件还有两个高频属性:AutoReverse和Loop。AutoReverse控制动画到达StopValue后是否自动反向播放回StartValue,Loop控制是否无限循环。它们非常适合做持续性的提示效果,比如“加载中的呼吸灯”“待命状态的脉冲环”。
但自动反向有个坑:动画结束时,属性停留在StartValue还是StopValue取决于播放轮次。AutoReverse播放一轮后属性回到起始值,如果你在结束事件里读取当前属性值来判断状态,需要小心。为了避免状态混乱,我建议Loop和AutoReverse只用于纯装饰性效果,业务状态切换还是用手动触发,自己控制Start和Stop。
事件方面,动画组件提供了ProcessAnimation事件和OnFinish事件。OnFinish在动画播放完一轮后触发,经常用在“动画结束后再做下一个动作”的场景,例如淡入完成后让输入框获取焦点。如果动画是Loop的,OnFinish不会触发,这时需要监听ProcessAnimation或者用计时器检查状态。
关于代码里动态创建动画组件,这里给一个实用的方式:
uses System.SysUtils, System.Types, FMX.Types, FMX.Ani; procedure TForm1.AnimateOpacity(const AControl: TFmxObject; const AOpacity: Single); var Anim: TFloatAnimation; begin Anim := TFloatAnimation.Create(AControl); Anim.Parent := AControl; Anim.PropertyName := 'Opacity'; Anim.StartValue := AControl.GetObject.Property('Opacity').GetObject.AsType<Single>; Anim.StopValue := AOpacity; Anim.Duration := 0.4; Anim.Interpolation := TInterpolationType.Sinusoidal; Anim.Start; end;这个方法的好处是不动设计器,可以动态为任意控件生成动画组件。注意Create的Owner参数填的是AControl本身,这样当控件销毁时动画组件会自动释放,不会泄漏。很多初学者会直接用Self作为Owner,结果控件释放了动画组件还活着,引发空引用。
2.4 透明窗体与Panel TopLevel子窗体的特殊场景
最近在Delphi社区里透明窗体、Panel做子窗体的讨论很多。这两个场景特别适合结合动画,也特别需要注意额外参数。
透明窗体通常需要设置窗体StyleBook为空、Transparency=True、Fill.Kind = None,并且去掉边框。在透明窗体上做动画,Opacity效果会作用于整个窗体,但要注意:如果窗体内部有Panel作为内容容器,你希望淡入的是窗体还是Panel?从性能角度,我建议把透明度动画作用在Panel上,而不是窗体本身。因为窗体级别的Opacity会触发整个窗体重绘,而Panel级别的动画只需要重绘局部区域。
用Panel模拟子窗体切换时,动画目标通常是Panel的Position.X或Margins.Left。FMX里TForm有TopLevelControls机制,但直接把多个Panel放到同一个窗体上,用动画切换它们的Offset是更高效的做法。做法是把多个Panel叠放,让非活跃Panel的Opacity设为0,动画时把Opacity拉回1并改变位置。这样不必创建真正的子窗体,也能达到类似页面切换的效果。
这里有个关键点:如果Panel使用了Align=Client,直接动画Position是不行的,因为布局系统会覆盖坐标。解决方法是嵌套一个容器,外层Panel负责布局(Align=Client),内层Panel负责动画(Position和Opacity都由动画控制)。这个嵌套思想在FMX里非常重要,几乎所有复杂动画布局都要靠它。
3. 一步一步给窗体添加动态效果:四个可直接抄作业的实例
3.1 效果一:窗体淡入淡出
这个效果最基础,几乎所有工具类软件都会用到。我以主窗体Form1为例,目标是让它启动时淡入,关闭时淡出。
首先,在设计器里给Form1添加一个TFloatAnimation,我习惯命名为FloatAnimFormOpacity。PropertyName设为Opacity,StartValue设为0,StopValue设为1,Duration设为0.4,Interpolation选Sinusoidal。注意一点:窗体默认Opacity是1,如果想让启动时淡入,还需要在窗体的OnCreate或FormShow事件里先把Opacity设为0。
这里有一个容易出错的地方:窗体的OnShow事件每次切换窗口都会触发,如果你的设置面板在窗体显示时把Opacity设为0,再配合动画播放,可能会导致每次激活窗体都闪烁。更稳妥的方式是在OnCreate里,或者在第一次显示之前用PostMessage机制延迟处理,确保动画组件在窗体句柄创建完成后才启动。
关闭时淡出更讲究。如果在FormClose里做淡出动画,可能因为窗体销毁太快,动画只播了一半。我的做法是拦截WM_CLOSE或者重写Close行为,先用动画把Opacity降到0,然后在OnFinish事件里调用真正的Close。也就是“手动不给窗体立即关闭,先播完动画”。
procedure TForm1.CloseAnimated; begin FloatAnimFormOpacity.StartValue := Self.Opacity; FloatAnimFormOpacity.StopValue := 0; FloatAnimFormOpacity.OnFinish := DoRealClose; FloatAnimFormOpacity.Start; end; procedure TForm1.DoRealClose(Sender: TObject); begin Self.Close; end;这段代码需要说明的是StartValue不是写死1,而是读取当前Opacity。因为用户可能在界面停留很久,中途可能通过代码改过Opacity,或者弹窗未完全显示时就点了关闭,这时如果从1开始播放,画面会突然闪亮一下。
3.2 效果二:缩放弹出(Modal风格的友好感)
第二个常用效果是缩放弹出,通常用于自定义消息框、确认框、设置面板。目标是让一个Panel从0.85倍大小柔和的放大到1倍,同时透明度从0变为1,整个弹出过程大约0.3秒。
这里就要用到Scale动画。FMX控件的Scale属性是TScale类型,包含X和Y两个分量。动画组件无法直接驱动TScale类型,所以要分别建两个TFloatAnimation:一个PropertyName设为Scale.X,另一个设为Scale.Y。缩放中心也需要单独设置,否则会从左上角缩放,看起来像是窗角抽搐。正确的做法是先把Panel的ScaleCenter.X和ScaleCenter.Y都设为0.5,这代表缩放的中心点位于Panel正中央。
实际操作中,我还会给这个效果加一个ColorAnimation做背景遮罩的淡入。在Panel下面放一个矩形Rectangle,设置圆角,Fill颜色为白色。ColorAnimation将矩形的透明度从0动画到0.8,形成遮罩效果。这样看起来就像原生模态弹窗一样,背景变暗、内容从中心放大,用户注意力自然聚焦到弹窗上。
这个效果的Trigger设置建议完全用手动。在打开弹窗按钮的OnClick里,先设置Panel.Visible := True,Opacity := 0,Scale.X := 0.85,Scale.Y := 0.85,然后依次启动ColorAnimation遮罩淡入、FloatAnimation淡入、Scale动画。关闭时反向播放。
动画之间“串联”的顺序也有讲究。我习惯让遮罩先淡入(大约0.15秒),随后内容Panel开始放大(0.2秒),形成“背景先沉、内容后起”的层次感。如果同时播放,两个动画的起始节奏会互相冲突,视觉上显得发飘。
3.3 效果三:滑入滑出(配合Panel做子窗体切换)
再进阶一点,用动画改造表单页面切换。很多人问过“Panel TopLevel子窗体”怎么用,其实FMX里做类似WinForm TabControl的页面切换,最顺手的方案就是叠放Panel加位置动画。
我这里说的结构是:窗体上一个根容器,里面放两个Panel,分别是PanelLogin和PanelMain。初始PanelLogin可见,PanelMain不可见。切换时,PanelMain先设置Left位置等于窗体的宽度(也就是SlideInStart),再设置Visible := True,Opacity从0变到1,Position.X从窗体宽度动画到最终位置0,形成从右边滑入的效果。原来的PanelLogin向左滑出或直接淡出。
关键点在Align处理。我的经验是:PanelMain不要直接设置Align=Client,否则位置动画无效。给PanelMain外面再套一层容器,比如一个Rectangle,这个Rectangle保持Align=Client,位置不动,真正执行动画的是PanelMain本身。
举一个具体的参数组合。窗体宽度假设800,PanelMain.Width也是800,切换开始时:
PanelMain.Visible := True; PanelMain.Opacity := 0; PanelMain.Position.X := 800; // 从右侧屏幕外开始然后动画StopValue设为0,Scale.X和Scale.Y同时从0.98放大到1,形成“滑入+轻微放大”的双重效果。整个动画Duration设0.5秒,Interpolation设Cubic,会有明显的“先加速后减速”的拖拽质感。这种组合视觉效果非常接近移动端页面滑动切换。
如果你的需求是侧边抽屉,原理一样,只是把方向从左右改成上下、从改Position.X改成Position.Y。
3.4 效果四:背景色渐变的透明窗体
这个效果结合了最近热词里的透明窗体和背景图需求。需求背景通常是:窗体无边框、Transparency设为True、背景色从一种颜色平滑过渡到另一种颜色,中间还能显示半透明背景图。
透明窗体的核心设置先说清楚:Form1.Transparency := True;Form1.Fill.Kind := TFillKind.None;Form1.BorderStyle := TFormBorderStyle.None。如果还用StyleBook,透明窗体效果会被StyleBook覆盖,所以通常要确保StyleBook是空的或者不设置。
背景色渐变用TColorAnimation,PropertyName设为Fill.Color。注意你是给窗体还是给窗体内部的Rectangle做动画?如果目标是窗体本身的Fill,那么PropertyName是Fill.Color。如果窗体内部有一个Rectangle用来盖住整个客户区,则属性名是Fill.Color,动画组件Parent放到Rectangle上。
这里有一个透明窗体特有的坑:大多数情况下,Windows窗口系统原生不支持窗口内某个区域的透明度渐变,FMX是通过分层窗口和重绘模拟透明度。当你把窗体Transparency设为True后,界面上的所有文字和控件都会被当作一张位图的一部分参与混合。如果里面嵌入了系统原生控件(如TEdit的WinOS版本),可能不支持透明混合。FMX中在透明窗体上优先用FMX原生控件,避免使用Windows原生TEdit、TComboBox在Transparency窗口上出现黑边。
我做播放器控件面板的时候就遇到这个情况,底部操作栏的背景色要随播放状态变化:播放中偏红,暂停偏蓝。用TColorAnimation加上Loop和AutoReverse,从0.8秒到0.8秒来回渐变,整体效果非常自然,用户感知不到颜色跳变。
3.5 用代码批量创建动画:摆脱设计器拖控件
有些场景:从后台返回的状态需要动态决定动画的目标值,比如不同用户权限展示不同的引导动画;或者列表项多到不可能在设计器里逐项设置。这时推荐用TAnimator的静态方法,它用一个方法调用就能完成一次动画,不需要在窗体上增加额外组件。
TAnimator类位于FMX.Ani单元,最常用的是AnimateFloat:
uses FMX.Ani, FMX.Objects; procedure TForm1.AnimatePanel(APanel: TPanel); begin TAnimator.AnimateFloat(APanel, 'Opacity', 1.0, 0.5, TAnimationType.Out, TInterpolationType.Sinusoidal); TAnimator.AnimateFloat(APanel, 'Scale.X', 1.0, 0.4, TAnimationType.Out, TInterpolationType.Back); end;这些静态方法会自动创建并释放临时动画组件,省去手工管理生命周期。它的缺点是难以精细控制事件回调,设置OnFinish比较麻烦。所以我的原则是:简单临时效果用TAnimator,涉及状态联动和事件序列的,老老实实放置命名好的动画组件。两条路各有适用场景,不要混用。
4. 常见问题与排查技巧实录
4.1 动画不生效、动画闪跳的排查路径
很多初学者遇到“动画组件拖上去了,但运行时没有任何反应”。我总结了几条高频原因,按检查优先级排序:
Properties属性名写错。这是最高频的坑。FMX属性名严格区分大小写,且必须与运行时属性名完全一致。例如透明度属性是Opacity,不是opacity;位置属性是Position.X,不是Position.X;颜色属性是Fill.Color,不是Color。写错时编译并不会报错,但运行时动画组件找不到属性,直接失效。
动画组件的Enabled为False。有些动画组件在设计器默认Enabled是True,但也经常因为多次复制粘贴操作导致Enabled被置为False。检查GroupIndex和事件干扰也在这里。如果多个动画组件共享一个GroupIndex,那么GroupIndex相同的动画组件会互斥,每次只能有一个播放。
属性被布局系统覆盖。前面反复强调的Align=Client问题,是最常见的“动画闪跳”根源:动画明明播放了,但控件坐标又被Align强制拉回。解决方法是额外嵌套容器,或者动画期间临时把Align设为None。
窗体的Opacity=0但动画没触发。如果动画组件设了Trigger=IsVisible,并且TriggerTarget指向窗体本身,但窗体的Visible属性在创建时已是True,Trigger只在状态变化时触发一次,启动时不会有反应。这个时候需要手动Start,或者用FormShow事件里调用动画组件的Start。
我写了一个简短的自检清单,出现问题按顺序排查:
| 排查项 | 检查方法 | 常见坑 |
|---|---|---|
| PropertyName属性名 | 对照对象监视器里的实际属性名 | 大小写错误、缺少父路径 |
| Enabled和Trigger | 确认动画组件Enabled是True,Trigger类型和TriggerTarget匹配 | Trigger在不需要时干扰动画 |
| Align与布局 | 检查动画目标控件是否被Align控制 | Align=Client时Position动画无效 |
| Duration和插值 | 确认Duration大于0,插值类型不是Out/In为ExactlyNone | Duration为0导致瞬间跳变 |
| Owner生命周期 | 确认动画组件的Owner是目标控件本身 | Owner释放后产生空引用 |
4.2 读FMX动画源码的几个高价值入口(Delphi 12)
Delphi 12发布后,FMX源码可以直接打开,这是排查问题和学习动画底层原理的绝佳入口。我经常看的几个文件都在安装目录下的Source\fmx里。
比如FMX.Types.pas,里面定义了TFmxObject和属性系统的基础架构,动画组件本质就是通过这个属性系统动态修改控件属性的。理解它,你就明白为什么PropertyName写错不报错。
再看FMX.Ani.pas,定义了TAnimation、TFloatAnimation、TColorAnimation这些类。我在里面看到过TAnimator的实现,它每次调用静态方法时,会创建一个临时的TAnimation对象,播放结束后释放。这解释了一个问题:TAnimator无法获取动画组件的持续引用,想中途停止不太好操作。
还有一个高价值入口是FMX.Objects.pas里的TShape、TRectangle的Fill属性定义。你会在里面看到Fill.Kind可以设置成None、Solid、Gradient、Bitmap等。ColorAnimation的目标Fill.Color只在Fill.Kind=Solid时有效,如果你把Fill.Kind设为Gradient或Bitmap,ColorAnimation会失效。这个细节文档里很少写,源码里一眼就能看到。
源码阅读的方法上,我的建议是从一个简单动画组件开始,比如HFloatAnimation,在它的Start方法上下断点,观察它怎么创建插值器、怎么驱动目标属性。跟着源码走一遍,很多玄学问题都会豁然开朗。
4.3 性能优化:透明窗体与子窗体动画的坑
动画卡顿不一定是因为帧率低,更常见的是“界面重绘面积过大”。FMX渲染性能最怕大面积的渐变和透明混合,尤其在跨平台环境。
透明窗体上做全窗体级别的Opacity动画,等于每一帧都要把整个窗体的内容与桌面背景重新混合,这是一个相当昂贵的操作。如果窗体里又有大量文字和控件,这个成本还会翻倍。我的优化思路是避免全窗体动画,尽量把动画限制在某个局部容器里。例如想让通知栏淡入淡出,可以只对通知栏所在的Rectangle做动画,而不是整个窗体。
Panel顶替子窗体做页面切换时,也要注意:旧的Panel如果还Visible,并且Opacity还没降为0,它会和新的Panel叠在一起,增加一次半透明混合。我在项目里特别要求:切换动画结束后,立刻把旧Panel的Visible设为False,避免后续无谓的混合开销。
另一个常被忽视的性能点是内存位图的缓存。如果动画中涉及尺寸变化,FMX需要连续重新布局和栅格化。Scale动画如果发生在包含大量文字的Panel上,文字会逐帧重新栅格化,导致肉眼可见的模糊闪动。解决方案有两个:一是动画期间给Panel设置一个截图纹理(通过TRenderTarget或Canvas的ToBitmap)作为静态图片,动画结束后再恢复真实控件;二是尽量少对带文字的复杂容器做Scale动画,改Position动画,因为位置变化不涉及重栅格化,性能开销小很多。
4.4 动画与布局属性冲突的避坑清单
动画并不是独立存在的,它会影响控件的布局状态,反过来布局状态也会破坏动画。这一节把我积攒的冲突案例整理成清单,当作最后一道防线。
第一条:动画期间尽量不要让用户调整窗体大小。窗体的Align布局在窗体尺寸改变时会对所有子控件重新布局,这会把正在动画中的Position和Size全部打回原样。如果用户有拖拽缩放宽高的需求,最稳妥的方案是在动画播放期间暂时禁用窗体边框的Resize,或者动画完成后用代码重新同步一次目标值。
第二条:ControlType与Scale动画的关系。FMX控件有两种ControlType:Platform和Styled。Platform类型在Windows下会创建原生Win32控件句柄,当你对该控件做Scale动画时,原生窗口的尺寸通常不会实时跟随FMX的视觉变换,经常出现视觉位置和实际点击区域不一致。解决的办法是使用ControlType=Styled,让FMX自己绘制所有内容,这样Scale变换对点击区域同样生效。做窗体级动画,尤其是缩放动画时,尽量全部使用Styled控件。
第三条:颜色动画对控件的状态颜色有影响。比如TButton有文本颜色、背景色、边框色等,多个属性本身由Style中的触发器管理。当你用ColorAnimation对Button的TextColor做动画时,如果Button状态(Normal/Hover/Pressed)发生变化,Style会强写回颜色,动画就会被强行终止。所以对Styled控件做颜色动画,最好在Style的Normal状态禁用Style自身的颜色更新,或者把动画应用在自定义Rectangle上,而不是标准控件上。
第四条:尽量把动画组件放在目标控件的子节点下。在设计器里很多人习惯把动画组件都拖到窗体上,这样所有动画平级排列,管理起来不直观。更好的做法是把动画组件拖放到其目标控件的下层级,例如要动画Panel1的透明度,就把FloatAnimation放进Panel1的子节点。这样做的好处是:Panel1销毁时动画组件自动释放,且RBTree遍历顺序清晰,不容易跟其他动画冲突。
最后再分享一个小技巧,这个技巧帮我排查过无数次问题:FMX设计器里,动画组件运行时如果不希望它出现在Structure面板,把它的DesignVisible设为False即可。这样界面结构更清爽,动画组件少时也许无所谓,但动画一多,这个习惯能大大提升维护效率。
我在实际使用中还有一个体会,动画效果是做给用户看的,而不是做给自己看的。参数上再完美,如果动画拖慢了操作节奏,用户照样会烦。所以每次做完动画,我都建议用一个普通配置的机器跑一遍真实业务路径,确认从点击到结果出来的总时间没有因为动画而显著增加。0.3到0.5秒的动画最安全,超过了这个区间,就要警惕它是否已经影响到了生产操作。现在FMX把动画的门槛压得足够低,真正拉开差距的,其实是动画的取舍和节奏控制。愿你也能在你的窗体上做出那种刚刚好的动态效果。