☰
Delphi窗口动画实战:AnimateWindow、分层窗口与缓动函数
2026/11/3 15:53:40 网站建设 项目流程

窗口动画这件事,在Delphi里从来不是"调个API就完事"的活儿。我见过太多项目,一开始兴冲冲地加上AnimateWindow,结果在客户机器上要么一闪而过,要么半透明残影,要么在多屏环境下飞到屏幕外面去了。真正把它做稳,需要弄清楚三件事:系统提供的动画能力边界在哪、分层窗口的alpha通道怎么逐帧驱动、以及定时器与窗口重绘之间的时序关系怎么协调。这篇内容就把我在Delphi里实现动画窗口的整套思路拆开讲——从最省事的AnimateWindow调用,到手动控制WS_EX_LAYERED做淡入淡出,再到用缓动函数驱动位置滑入与尺寸缩放。适合已经写过VCL窗体程序、想把交互质感往上抬一档的开发者参考,也适合刚开始接触Windows窗口消息机制、想搞明白"透明窗口到底怎么透明"的新手。代码全部基于Object Pascal,可以直接在IDE里跑起来验证。

1. AnimateWindow的甜区与它的硬天花板

AnimateWindow是Windows提供的一个窗口动画函数,Delphi里通过Windows单元直接引入,一行调用就能让窗体淡入、滑入或者从中心展开。它的好处是零依赖、零额外代码,坏处是控制粒度极粗——你只能告诉系统"用哪种效果、持续多久",没法指定起始透明度、没法中途打断、也没法做非线性缓动。很多教程到这里就结束了,但真正上项目你会发现,这个函数的限制远比想象中多。

1.1 最小可用示例与它的实际表现

最典型的调用长这样:

procedure TForm1.FormShow(Sender: TObject); begin AnimateWindow(Handle, 300, AW_BLEND); end;

把这段代码放进FormShow,运行时窗体会在300毫秒内从全透明渐变到不透明。看起来挺美,但这里有个坑:FormShow触发时窗体还没有真正完成绘制,如果窗体上有较重的控件(比如大表格、图像控件),动画过程中会看到控件区域出现未绘制的空白块,动画结束后才补上。原因是AW_BLEND本质上是让系统对窗口做整体alpha混合,窗口内容本身并不会因为动画而延后绘制,控件自己的WM_PAINT还在排队。

我的处理办法是在FormShow里先把Visible设为False,等OnActivate或者一个短延时后再触发动画:

procedure TForm1.FormShow(Sender: TObject); begin Visible := False; PostMessage(Handle, WM_USER + 100, 0, 0); end; procedure TForm1.WndProcUser(var Msg: TMessage); begin if Msg.Msg = WM_USER + 100 then begin ShowWindow(Handle, SW_SHOW); AnimateWindow(Handle, 300, AW_BLEND); end; inherited; end;

用PostMessage把显示动作推到消息队列末尾,给控件绘制留出时间,实际测试下来空白块基本消失。这是一个很典型的"函数能用"和"函数好用"之间的差距。

1.2 AW_BLEND和AW_SLIDE为什么不能混用

AnimateWindow的dwFlags参数里,AW_BLEND(淡入淡出)、AW_SLIDE(滑动)、AW_CENTER(中心展开/收缩)属于互斥的"动画类型"位。文档上写得很含蓄,实际测试会发现:同时传入AW_BLEND or AW_SLIDE时,系统会静默选择其中一个执行,具体是哪个在不同Windows版本上表现还不一致。我在Win10和Win11上分别试过,行为确实有差异——这种不确定性在交付项目里是致命的,因为用户机器上的系统版本你控制不了。

所以我的原则很简单:一次动画只用一个类型位。如果需要"淡入+滑入"的组合效果,就别指望AnimateWindow,直接走分层窗口自己画。滑动方向由AW_HOR_POSITIVE、AW_HOR_NEGATIVE、AW_VER_POSITIVE、AW_VER_NEGATIVE这四个位控制,配合AW_SLIDE使用,窗口从对应方向滑入。滑动动画在视觉上比淡入更有"进场感",但如果窗体尺寸较大,滑动过程中会明显看到窗体边缘拖拽的残影,尤其在低端显卡或远程桌面环境下。

1.3 系统动画的三个绕不过去的限制

第一,不可中途打断。AnimateWindow是同步阻塞的,调用后当前线程会等到动画结束才返回。这意味着动画期间你的主线程无法响应其他消息,用户点击关闭按钮也得等动画放完。对于300毫秒的短动画影响不大,但如果想做600毫秒以上的慢动画,体验会明显变差。

第二,不能反向控制。你没法在动画进行到一半时让它反向播放,也没法动态调整透明度曲线。系统给什么就是什么。

第三,部分窗口类型不支持。子窗口、某些带有特殊样式的窗口调用AnimateWindow会直接返回失败,而且不报错,只是什么都没发生。我吃过这个亏——给一个WS_EX_TOOLWINDOW的浮动面板加动画,调试了半小时才发现是样式冲突。判断是否支持,最稳妥的办法是看返回值,False就说明这次动画没生效,要准备降级方案。

注意:AnimateWindow在系统设置里关闭了"动画效果"时会静默失效(比如用户开了"减少动画"的无障碍选项)。如果你的界面逻辑依赖动画完成事件,务必用定时器兜底,不要假设动画一定执行。

2. 分层窗口:把透明度从"整体"做到"逐帧可控"

想要真正控制动画的每一个中间状态,就得自己接管透明度。Windows的WS_EX_LAYERED扩展样式就是干这个的,开启后可以给窗口设置一个全局alpha值,实现整窗半透明。Delphi里通过GetWindowLong和SetWindowLong操作扩展样式,再调用SetLayeredWindowAttributes设定alpha。这套组合是做淡入淡出动画的核心。

2.1 开启分层样式的正确姿势

很多人第一次写会踩一个坑:在FormCreate里直接调SetWindowLong(Handle, GWL_EXSTYLE, ...),此时窗体句柄可能还没真正创建完成,或者样式被VCL自己的初始化流程覆盖。正确的时机是在Handle已经分配之后,比如OnCreate里,但要用GetWindowLong先读出原样式再按位或,而不是直接赋值:

procedure TForm1.FormCreate(Sender: TObject); var ExStyle: LongInt; begin ExStyle := GetWindowLong(Handle, GWL_EXSTYLE); SetWindowLong(Handle, GWL_EXSTYLE, ExStyle or WS_EX_LAYERED); SetLayeredWindowAttributes(Handle, 0, 0, LWA_ALPHA); // 初始全透明 end;

这里SetLayeredWindowAttributes的第三个参数是alpha值,0到255,0完全透明、255不透明。第四个参数LWA_ALPHA表示"按整体alpha混合",如果传LWA_COLORKEY则是"把某个颜色键抠成透明",两者用途完全不同。动画淡入淡出只用LWA_ALPHA。

有个细节值得说:一旦窗口带上了WS_EX_LAYERED,它的绘制会走一条额外路径,性能比普通窗口略低。所以动画结束后我通常不会移除这个样式,因为移除会导致窗口重新绘制一次,反而闪烁;但如果窗口长期不需要透明度,最好在创建时就判断,别无谓地加。

2.2 用定时器逐帧推进alpha

透明渐变的本质是把0到255的alpha值分成若干帧,每帧更新一次。用TTimer是最简单的做法:

type TForm1 = class(TForm) Timer1: TTimer; procedure FormCreate(Sender: TObject); procedure Timer1Timer(Sender: TObject); private FAlpha: Byte; FTargetAlpha: Byte; FStep: Integer; end; procedure TForm1.FormCreate(Sender: TObject); begin SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) or WS_EX_LAYERED); FAlpha := 0; FTargetAlpha := 255; FStep := 17; // 255 / 17 ≈ 15帧 SetLayeredWindowAttributes(Handle, 0, FAlpha, LWA_ALPHA); Timer1.Interval := 16; // 约60fps Timer1.Enabled := True; end; procedure TForm1.Timer1Timer(Sender: TObject); begin if FAlpha < FTargetAlpha then begin FAlpha := FAlpha + FStep; if FAlpha > FTargetAlpha then FAlpha := FTargetAlpha; SetLayeredWindowAttributes(Handle, 0, FAlpha, LWA_ALPHA); end else Timer1.Enabled := False; end;

Timer1.Interval := 16对应大约60帧每秒,这是视觉上比较顺滑的节奏。FStep := 17意味着15帧左右走完整个渐变,总时长约240毫秒。为什么不设Interval := 1追求极致平滑?因为TTimer的精度受系统时钟粒度限制,实际触发间隔通常在15毫秒上下浮动,设得太低只是徒增消息负担。用16毫秒是个经验值,刚好卡在系统时钟的一个tick附近。

这里有个反直觉的点:不要每帧都调用Invalidate。SetLayeredWindowAttributes本身会触发窗口合成层更新,不需要你主动重绘窗口内容。如果你在里面又加了Repaint,会看到明显的闪烁,因为内容重绘和透明度更新在争抢同一帧。我一开始就是这么干的,结果淡入过程像在打嗝。

2.3 UpdateLayeredWindow路线和它的适用场景

SetLayeredWindowAttributes做的是"整窗统一alpha",够用但不是万能的。如果你要做的是不规则形状窗口、带阴影的异形窗体、或者要在透明区域上叠自定义绘制内容,就得走UpdateLayeredWindow这条路线——它要求你提供一个32位带alpha通道的位图,然后把整张图作为窗口内容提交。这种方式的控制力最强,每一像素的透明度都能自己定,但代价是窗口的WM_PAINT绘制流程基本失效,所有内容都得自己画到位图上。

我的建议很明确:做进场/退场动画、模态遮罩、提示气泡这类需求,用SetLayeredWindowAttributes就够了,别上UpdateLayeredWindow。后者代码量翻好几倍,还要处理位图的预乘alpha,调试成本高。只有当你需要窗口本身有非矩形轮廓时,才值得投入。我做过一个圆角悬浮提示窗,用UpdateLayeredWindow配合GDI+绘制圆角矩形和阴影,效果确实好,但那一套代码抽出来后单独成了一个小模块,不适合塞进普通业务窗体里。

3. 位置滑入与尺寸缩放:把线性变化做出质感

淡入淡出靠alpha,位移和缩放则靠SetWindowPos。核心思路是把起始位置到目标位置之间的路径按时间切分,每帧算一个中间点,然后移动窗口。听起来简单,做出来好不好看,全在"怎么切"这件事上。

3.1 用WM_TIMER驱动SetWindowPos的骨架

type TSlideAnim = class private FForm: TForm; FStartPos, FEndPos: TPoint; FStartTime: Cardinal; FDuration: Integer; FTimer: TTimer; procedure OnTick(Sender: TObject); public constructor Create(AForm: TForm); procedure SlideTo(const Target: TPoint; DurationMs: Integer); end; constructor TSlideAnim.Create(AForm: TForm); begin FForm := AForm; FTimer := TTimer.Create(nil); FTimer.Interval := 16; FTimer.Enabled := False; FTimer.OnTimer := OnTick; end; procedure TSlideAnim.SlideTo(const Target: TPoint; DurationMs: Integer); begin FStartPos := Point(FForm.Left, FForm.Top); FEndPos := Target; FDuration := DurationMs; FStartTime := GetTickCount; FTimer.Enabled := True; end; procedure TSlideAnim.OnTick(Sender: TObject); var Elapsed, Total: Integer; T: Double; NewX, NewY: Integer; begin Elapsed := GetTickCount - FStartTime; if Elapsed >= FDuration then begin FForm.SetBounds(FEndPos.X, FEndPos.Y, FForm.Width, FForm.Height); FTimer.Enabled := False; Exit; end; Total := FDuration; if Total <= 0 then Total := 1; T := Elapsed / Total; NewX := Round(FStartPos.X + (FEndPos.X - FStartPos.X) * T); NewY := Round(FStartPos.Y + (FEndPos.Y - FStartPos.Y) * T); FForm.SetBounds(NewX, NewY, FForm.Width, FForm.Height); end;

这段是纯线性插值,动画本身没问题,但视觉上会显得"机械"——起步和停止都是硬邦邦的。原因很简单,真实世界的物体运动有加速和减速,人眼对匀速运动会有种"这不是动画,这是被拖过去"的错觉。

3.2 缓动函数:一行改动,观感天差地别

缓动函数的作用是把线性时间映射成非线性进度。最常用的是EaseOutCubic,起步快、收尾慢,适合"进入"类动画:

function EaseOutCubic(T: Double): Double; begin Result := 1 - Power(1 - T, 3); end; function EaseInOutQuad(T: Double): Double; begin if T < 0.5 then Result := 2 * T * T else Result := 1 - Power(-2 * T + 2, 2) / 2; end;

把OnTick里的插值改成先算缓动值再插:

T := Elapsed / Total; T := EaseOutCubic(T); NewX := Round(FStartPos.X + (FEndPos.X - FStartPos.X) * T); NewY := Round(FStartPos.Y + (FEndPos.Y - FStartPos.Y) * T);

从匀速换到EaseOutCubic,同样的距离和时长,观感差别非常明显:后者感觉窗口"弹"进了目标位置,而不是被推过去。这就是为什么现代UI框架几乎都内置了好几套缓动曲线——它是动画质感的第一道门槛。

我一般按场景选缓动:

场景推荐缓动理由
窗体进场EaseOutCubic快速到位,收尾稳,不拖沓
窗体退场EaseInCubic起步慢加速离开,观感干脆
尺寸缩放(放大)EaseOutBack轻微过冲再回弹,有弹性
尺寸缩放(缩小)EaseInQuad平滑收缩,避免突兀
位置微调(如吸附)EaseInOutQuad两端都柔,像被磁吸过去

EaseOutBack需要额外处理过冲,因为它会短暂超过1再回落到1,得用Math.Max和Math.Min把超出范围的值截断在合理区间,不然窗口会飞出去再回来。

3.3 SetBounds、SetWindowPos和DeferWindowPos怎么选

SetBounds是VCL封装好的方法,最简单,但每调用一次都触发一次窗口位置变更消息,动画过程中会频繁触发OnResize、OnMove。如果窗体上有依赖这些事件的逻辑(比如自动排版),可能会被高频触发拖慢。

SetWindowPos更底层,可以通过参数控制是否触发WM_WINDOWPOSCHANGING等消息,但需要自己算客户区尺寸:

SetWindowPos(Handle, 0, NewX, NewY, 0, 0, SWP_NOSIZE or SWP_NOZORDER or SWP_NOACTIVATE);

而DeferWindowPos系列适合"批量移动多个窗口"——比如主窗体和它的浮动工具条要一起滑入,用BeginDeferWindowPos起一个批次,DeferWindowPos逐个添加,EndDeferWindowPos一次提交,这样多个窗口在同一帧内同步移动,不会出现错位。

我在单窗体动画里用SetWindowPos,并配上SWP_NOACTIVATE避免每帧激活窗口抢焦点;在多窗体联动场景才上DeferWindowPos。SetBounds只在动画最后一帧用它做精确落位,因为那一次希望触发完整的VCL布局流程。

4. 实测踩坑:抖动、残影、多显示器和DPI

上面讲的都是"正向路径",但真实环境里翻车的地方往往在边界条件上。这一节把我在不同机器、不同系统版本上遇到的坑摊开说,都是有具体复现条件的。

4.1 淡入过程中的残影和闪烁

现象:窗体淡入时,背景区域会短暂看到上一次窗口位置的残留,像是粘贴了半张旧图。

根本原因通常是两层:一是窗口内容在透明度变化时没有强制重绘,合成器拿到了过期内容;二是TTimer的帧间隔与系统的合成节奏不同步,导致某些帧的alpha值跳变。

我的排查步骤是这样的:

  1. 先确认DoubleBuffered是否开启。VCL窗体的DoubleBuffered默认是False,开启后能消掉大部分自绘控件的闪烁。
  2. 再确认动画过程中没有任何Invalidate或Update调用。如果有,全部去掉,只保留SetLayeredWindowAttributes。
  3. 如果还有残影,检查窗体是否有TransparentColor或TransparentColorValue设置,这些设置和分层alpha会打架。
  4. 最后检查定时器的实际触发频率,可以用OutputDebugString打印每帧的时间戳,看是否有超过50毫秒的间隔跳变。

实测下来,第1条和第2条能解决八成问题。剩下两成多半是显卡驱动或远程桌面环境的合成延迟,这种情况下我会把动画帧数降低、单帧步长加大,用"少帧大步"换稳定,视觉上略糙但不会出残影。

4.2 多显示器下的坐标失控

这个问题在多屏拼接的办公机上特别常见。假设主屏在上、副屏在下,你把窗体从副屏底部滑入主屏,如果起始坐标算的是"屏幕相对位置"而不是"虚拟桌面绝对坐标",窗口就会滑到屏幕外或者出现在错误的位置。

Delphi里Screen.Width、Screen.Height只反映主显示器的尺寸,多屏环境下必须用Screen.DesktopWidth、Screen.DesktopHeight,它们表示整个虚拟桌面的范围。如果要做负坐标方向(主屏左侧还有一块副屏,副屏坐标是负数),更要小心:

function GetWorkAreaForWindow(AForm: TForm): TRect; var Mon: TMonitor; begin Mon := Screen.MonitorFromWindow(AForm.Handle, mdNearest); if Assigned(Mon) then Result := Mon.WorkareaRect else Result := Screen.WorkAreaRect; end;

用MonitorFromWindow拿到窗体当前所在的显示器,再用该显示器的WorkareaRect计算滑入路径,这样不管用户把窗体拖到哪块屏上,动画坐标都是对的。我踩过的最离谱一次是窗体从错误屏幕滑入后触发OnResize,把内部布局全打乱了,排查了半天才发现是坐标基准错了。

4.3 DPI缩放下的尺寸错位

高DPI环境下,窗体逻辑尺寸和物理像素尺寸不是一个概念。SetWindowPos接收的是物理像素,而Delphi在高DPI模式下窗体属性Left、Top、Width、Height是逻辑单位。如果你混着用,动画过程中窗体会越滑越大或越滑越小,看起来就像"呼吸"效果——当然不是你想要的。

解决办法统一在物理坐标层面操作:动画内部全部用GetWindowRect取到的实际像素坐标做插值,落到SetWindowPos,不去碰VCL的逻辑属性。如果非要读写Left/Top,就在同一台机器上保持单位一致,别在动画里一会儿逻辑一会儿物理。

还要注意Screen.PixelsPerInch在运行过程中可能变化(用户拖到不同DPI的屏幕),所以缓动时长要不要按DPI调整得看具体情况。我的经验是动画时长固定100到300毫秒之间,DPI变化不改变时长,只改变位移距离,这样观感一致性反而更好。

4.4 动画未完成就关闭窗口的资源清理

一个很隐蔽的崩溃来源:窗口在动画进行中被销毁,定时器还在跑,下一帧回调访问了已经释放的窗体句柄。典型场景是用户连点关闭按钮,或者程序化关闭时没跟动画状态同步。

处理原则是动画对象持有窗体的弱引用,且在窗体销毁前显式停止定时器:

procedure TForm1.FormDestroy(Sender: TObject); begin if Assigned(FTimer) then begin FTimer.Enabled := False; FreeAndNil(FTimer); end; end;

同时在定时器回调开头加一句if not Assigned(FForm) then Exit;。两重保险,因为VCL的销毁顺序在某些情况下会先释放子组件。我还见过一个更绕的情况:动画对象是用接口或对象池管理的,窗体销毁时没通知到,导致池里的定时器继续指向死句柄。这种就得在窗体销毁时主动调用动画对象的Detach方法,把引用置空。

5. 封装成组件:让动画不再是散落各处的胶水代码

第一次给项目加动画时,我是在每个窗体的OnCreate里直接写SetLayeredWindowAttributes和定时器逻辑。加到第五个窗体时,代码已经失控了——五个地方各自维护一套alpha步进,参数不统一,改一个效果要动五处。痛定思痛,抽了个TWindowAnimator出来。

5.1 接口设计:只暴露必要的几个方法

设计上我坚持"少即是多",对外只保留四个方法:

type TWindowAnimator = class public procedure FadeIn(DurationMs: Integer = 240); procedure FadeOut(DurationMs: Integer = 200); procedure SlideTo(const Target: TPoint; DurationMs: Integer = 300); procedure Stop; constructor Create(AForm: TForm); destructor Destroy; override; end;

FadeIn和FadeOut内部共用同一套alpha推进逻辑,SlideTo内部封装位置插值和缓动。调用方只关心"多久"和"到哪",不关心内部用什么定时器、什么缓动函数。这样上层业务代码干净,改动画参数也只改一个地方。

构造函数里做样式初始化,析构函数里清理定时器,把生命周期管理收拢到对象自身,避免调用方忘记释放。

5.2 动画队列与打断机制

真实业务里经常出现"动画还没播完又来一个新指令"。比如窗体正在淡入,用户点了按钮触发了淡出。如果不处理,两个动画会同时改alpha值,画面乱跳。

我的做法是维护一个待执行队列,每个动画带一个优先级和可打断标记:

procedure TWindowAnimator.Enqueue(const Anim: TAnimRequest); begin if FCurrent <> nil then begin if Anim.Priority >= FCurrent.Priority then begin FCurrent.Terminate; // 当前动画立即收敛到终点 FCurrent := nil; end else begin FQueue.Add(Anim); Exit; end; end; StartAnim(Anim); end;

打断时不是简单丢弃当前动画,而是让它跳到终点状态——这样窗口不会停在半透明或者半路上。这个细节很重要,否则打断后窗体可能卡在alpha=0.6的诡异状态。

5.3 和VCL重绘流程的协同

VCL的窗体有一整套自己的绘制消息链(WM_PAINT、WM_ERASEBKGND、WM_NCPAINT)。动画过程中,如果频繁触发这些消息,会跟合成器的更新抢资源。我在封装时做了两个约束:

第一,动画期间置一个FAnimating标志,在窗体的OnPaint里如果发现正在动画,就跳过非必要的重绘逻辑。第二,动画结束后再统一调一次Invalidate,让VCL走一遍完整的绘制流程,把动画期间可能积压的绘制请求一次处理掉。

这套协同做下来,窗体上就算有几十个控件,淡入过程也是干净的,不会出现控件逐个"冒出来"的阶梯感。不过我要提醒一句:如果你的窗体里有第三方控件(尤其是自绘的网格、图表),它们的绘制逻辑可能不认FAnimating标志,还是会有闪烁。这种情况就得在动画开始前用LockWindowUpdate临时冻结该控件,结束后解冻。LockWindowUpdate本身有争议——它在某些系统上会导致后续更新丢失——所以只在确认有问题的控件上局部使用,用完立刻解冻,不要全局挂。

6. 该不该做动画:性能开销和真实用户感受

写到这里得说句实话:不是所有窗体都值得加动画。我见过一个内部数据录入系统,每个弹窗都加了200毫秒淡入,结果操作员一天要开几十次明细窗,累加起来就是十几秒的纯等待。用户明确抱怨"每次点开都要等一下",最后全砍掉了。

我的判断标准有三条:

第一条,看触发频率。高频打开的功能窗体,动画时长压到80毫秒以内,或者干脆不做。低频的、有仪式感的场景(比如启动页、完成提示),可以放到300毫秒甚至更长。

第二条,看硬件下限。如果软件要部署到老旧的办公机、虚拟机、远程桌面环境,动画要做得更保守。远程桌面下合成器是服务端做的,alpha渐变尤其耗资源。我在一个虚拟机环境里实测过,150毫秒的淡入实际耗时接近400毫秒,因为帧被丢了一部分。这种情况下要么缩短动画,要么直接跳过。

第三条,看用户偏好。系统设置里有"在Windows中显示动画"的开关,可以通过SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, ...)读取。尊重这个设置,用户关了就别做。无障碍不是可选项,是基本素养。

function SystemAnimationsEnabled: Boolean; var Enabled: BOOL; begin Result := True; if SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, @Enabled, 0) then Result := Enabled; end;

在动画组件里一开头判断这个,返回False就立即把窗口置到终点状态,一帧都不走。这行代码很短,但能避免很多"为什么我的软件在客户机上闪个不停"的售后问题。

还有一个容易被忽略的点:动画对屏幕阅读器和键盘操作的影响。淡入过程中如果焦点已经可以到达窗体,键盘用户会在窗体还没完全可见时就开始操作,体验割裂。我的做法是动画期间禁用输入,动画结束后再SetFocus到默认控件。这个开关要和上面说的打断机制配合好,否则用户狂敲回车可能把窗体关掉。

最后分享一个我常用的手感调试小技巧:把动画时长临时调到2000毫秒,逐帧观察缓动曲线是不是符合预期,确认没问题后再改回200到300毫秒。短动画里很多曲线细微的毛病看不出来,拉长之后一目了然。这个办法帮我抓出过好几个缓动参数配错的问题,尤其是EaseOutBack的过冲系数,短动画里那个回弹几乎看不见,拉长才发现过冲幅度太大,窗口都飞到目标位置外面去了。

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

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

立即咨询