资讯动态

WPF动画从入门到实战:原理、MVVM与性能优化全攻略

发布时间:2026/10/9 8:36:37 来源:尧图企业网站定制
很多朋友一开始接触 WPF都会觉得界面死板、样式陈旧但其实 WPF 手里握着一张王牌就是动画。标题里的“WPF动画课”并不是让你学会花哨的炫技而是让你真正掌握让界面“动起来”的能力。本文我打算用一整套完整思路把 WPF 动画从底层原理到业务落地、从简单淡入淡出到 3D 看板、从纯前台代码到 MVVM 数据绑定全部串起来讲一遍。无论你是刚入门 WPF 的新人还是已经在做桌面应用但始终没把动画用好的老手这篇文章都能给你一些可以直接抄作业的方案更重要的是帮你避开那些我在实际项目里踩过的坑。1. WPF动画体系全景从几个基础概念开始1.1 动画的底层逻辑依赖属性、时钟与帧WPF 动画之所以比传统 WinForms 的 Timer 手动改写坐标要强大核心在于它构建在几个底层机制上依赖属性DependencyProperty、时钟Clock和 UI 线程的渲染循环。你不需要精通每一条运行机制但必须理解一个关键点WPF 动画不是每帧去改控件的普通属性而是作用于依赖属性的值。简单来说依赖属性支持一套完整的“值优先级”体系动画可以在某一时间段内临时覆盖属性的值动画结束后又可以自动还原。如果你写过DoubleAnimation会发现动画执行完后属性值会回到动画开始前的状态这背后就是依赖属性的“基础值”在起作用。理解了这一点你就不会在动画结束回调里傻傻地再去手动设置一遍终值而是要知道如何用FillBehaviorStop或者直接赋值来保留最终状态。另一个底层机制是时钟。每次你创建一个AnimationTimelineWPF 会为它生成一个Clock由Clock决定动画从什么时间开始、持续多久、是否重复、是否反转。这个模型和 HTML 里的 CSS 动画有些相似但更严格因为它是强类型、强时间轴的体系。我在实际项目里常用的做法是尽量用 XAML 里的Storyboard来组织动画因为它会把多个时钟编排成一个统一的时间逻辑。举一个生活化的类比动画就像安排一个舞台剧Storyboard是导演DoubleAnimation是演员时钟是播放时间表。导演决定了演员在什么时刻上台、什么时刻鞠躬下台、什么时刻重复表演。如果你的项目有多段动画需要先后播放用Storyboard就能清晰地控制先后顺序比在代码里一个个 await 更优雅。1.2 常用动画类型DoubleAnimation、ColorAnimation 与 PointAnimationWPF 内置的动画类型远比你想象的多。除了最常见的DoubleAnimation还有ColorAnimation、PointAnimation、ThicknessAnimation、Int32Animation、ObjectAnimationUsingKeyFrames等。它们的核心逻辑其实一样就是让一个属性的值在起止值之间平滑变化区别只在于属性类型不同。我刚接触的时候经常犯一个错误想改变一个控件的坐标却用TranslateTransform.X的依赖属性去配合DoubleAnimation这没问题但想改变多个属性时却一股脑写了五个DoubleAnimation导致代码冗长。后来才发现像ThicknessAnimation可以直接作用于Margin属性一下就能让控件在四方向上平滑移动。举个例子做一个简单的飘入动画Border x:NameCard Margin0,20,0,0 Border.Triggers EventTrigger RoutedEventLoaded BeginStoryboard Storyboard ThicknessAnimation Storyboard.TargetPropertyMargin From0,50,0,0 To0,20,0,0 Duration0:0:0.4 / /Storyboard /BeginStoryboard /EventTrigger /Border.Triggers /Border这样一句 XAML 就能让卡片从下方 50 像素的位置平滑移动到目标位置。为什么选ThicknessAnimation而不去改RenderTransform因为Margin参与布局动画会带动整体布局而RenderTransform只是在渲染阶段移动元素不影响布局。如果你做的是悬浮提示、临时标记用RenderTransform性能更友好如果你做的是类似手风琴展开那种需要推动后续元素的效果就得用Margin或Height。ColorAnimation则更多用于背景色、前景色的渐变过渡比如按钮 hover 时的变色。建议配合SolidColorBrush使用注意不要直接改Brush的Color而不设置起止值否则会生硬。PointAnimation常用于折线图、雷达图的数据点移动。做动态数据看板的时候我用它来让图表上的标记点平滑地从旧位置跳到新位置而不是瞬间闪过去。这类动画有一个特点目标属性通常不是标准依赖属性而是自定义控件或附加属性上的依赖属性。只要该属性用DependencyProperty.Register注册过WPF 的动画引擎就能覆盖它。1.3 在 XAML 里写动画和在代码里写动画的正确姿势很多新手最困惑的是到底该把动画写在 XAML 还是在 C# 代码里我的答案很直接优先写 XAML除非你需要在运行时动态生成。XAML 里可以直接用BeginStoryboard加EventTrigger实现“加载时播放”“鼠标悬停时播放”“点击时播放”。比如上面那个例子就写在Loaded事件里。代码里写动画的典型场景是动画的参数需要根据业务数据变化比如进度条的百分比、仪表盘的指针角度。这时我会用Storyboard.SetTarget和Storyboard.SetTargetProperty来动态创建。需要注意几点代码里创建的Storyboard需要手动Begin()并且最好保存在一个字段里方便后续停止、暂停。动态创建动画时To和From都要明确赋值否则 WPF 会尝试解析当前值但在某些复杂模板里可能取到非法值。用完的Storyboard建议调用Remove()避免形成对 UI 元素的隐式引用造成内存泄漏。我自己的习惯是把动画封装成自定义的一个AnimationService对外暴露PlayFadeIn(UIElement element, double duration)之类的方法。这样做的好处是业务代码里不会到处散落 Storyboard 创建逻辑也方便单元测试和后续替换为第三方动画库。2. 让动画“听话”缓动函数与关键帧2.1 缓动函数背后的人性化体验如果所有动画都是匀速的界面会显得非常机械。真实世界里的物理运动都有加速减速过程WPF 里的IEasingFunction就是为了模拟这种过程。LinearDoubleEasing之外内置了QuadraticEase、CubicEase、BounceEase、ElasticEase、BackEase等一大堆缓动函数它们能分别实现先快后慢、先慢后快、回弹、震荡、超调等效果。用起来很简单DoubleAnimation Storyboard.TargetPropertyOpacity From0 To1 Duration0:0:0.5 DoubleAnimation.EasingFunction CubicEase EasingModeEaseOut/ /DoubleAnimation.EasingFunction /DoubleAnimation为什么缓动函数重要因为人眼对速度变化的敏感度远高于颜色变化。一个缓慢淡入又带一点退势的效果会让界面有种“呼吸感”用户会觉得这个应用轻快、流畅。我做过一个数据对比功能表格行切换数据时如果直接瞬变用户很难注意到是哪一行变化了给行加一个短暂的背景色高亮动画先用ColorAnimation把背景改成浅黄色再慢慢淡回白色用户一目了然。这个技巧叫“行为感知动画”在数据密集的桌面应用里特别有用。选择缓动函数也有经验EaseOut适合元素飞入、面板弹出因为它在结束后几乎没有回弹感优雅克制EaseIn适合元素被收起、退场给人一种自然地加速离开的感觉EaseInOut适合来回重复的循环动画比如 loading 的旋转、呼吸灯光。BounceEase适合做轻量提醒但别滥用否则会显得轻浮。BackEase会让元素先朝反方向移动一小段再前进适合表达“释放”的感觉比如抽奖结果弹出、通知栏滑入但幅度要小。2.2 关键帧动画无法用单一插值完成的运动轨迹有时候我们需要元素沿着一条折线或曲线运动比如消息气泡从右上角飘到左下角中间还要转个弯。DoubleAnimationUsingKeyFrames这类关键帧动画就派上用场了。关键帧让你在不同时间点指定不同值WPF 会自动在相邻关键帧之间插值。关键帧的插值方式由帧的类型决定LinearDoubleKeyFrame线性插值适合路程图。DiscreteDoubleKeyFrame跳变适合不连续切换比如状态文字。SplineDoubleKeyFrame贝塞尔插值可以做出先快后慢的复杂曲线。EasingDoubleKeyFrame带缓动的关键帧是常用组合。做一个多关键帧位移Storyboard DoubleAnimationUsingKeyFrames Storyboard.TargetProperty(Canvas.Left) Storyboard.TargetNameBall LinearDoubleKeyFrame KeyTime0:0:0.5 Value100/ SplineDoubleKeyFrame KeyTime0:0:1.0 Value200 KeySpline0.2,0.0 0.8,1.0/ EasingDoubleKeyFrame KeyTime0:0:1.5 Value300 EasingDoubleKeyFrame.EasingFunction CubicEase EasingModeEaseOut/ /EasingDoubleKeyFrame.EasingFunction /EasingDoubleKeyFrame /DoubleAnimationUsingKeyFrames /Storyboard这里关键的点是KeySpline的用法。KeySpline定义了一个三次贝塞尔曲线控制点在 0~1 之间x 轴代表时间进度y 轴代表值进度。你可以试着给两组不同的 KeySpline比如0.2,0.0 0.8,1.0和0.4,0.0 0.6,1.0前者更“猛”后者更“稳”。做复杂动画时我建议先用 Blend即 Expression Blend可视化拖拽曲线再把数值抄回 XAML。但不要依赖 Blend因为运行时 KeySpline 的值是可以动态调整的完全可以用代码计算。2.3 动画的时长、重复、反转与暂停控制动画控制是界面反馈的灵魂。Duration 自不必说RepeatBehavior 和 AutoReverse 也很常用。RepeatBehavior有三种典型用法一个TimeSpan表示持续多久例如RepeatBehavior0:0:2表示动画播放 2 秒不管内部 iterations。一个整数加x表示重复次数例如RepeatBehavior3x。Forever无限循环适合 loading。AutoReverseTrue会让动画播放到终点后自动来回一次合起来动作更自然。做阶段性动画时不要把所有动画都做成Forever哪怕是悬浮在元素上也要记得动画结束后释放。否则在低配置机器上会一直消耗渲染资源。暂停和恢复通常用Storyboard.Pause、Storyboard.Resume也可以直接Begin重新开始但重新开始会让动画回到初始状态。我见过不少人做“点击后又快速点击”导致动画加倍闪烁就是因为没做暂停停止。正确做法是触发新动画前先调用Storyboard.Stop再重新Begin。另外用代码改变动画时长也是常见需求。比如给一个通知条设置展示时长过期后自动消失。你可以在代码里直接修改Timeline.Duration。但注意如果你已经Begin了修改 Duration 是没用的必须Stop之后再重新Begin。3. 与数据绑定和 MVVM 共舞动画在业务界面中的正确姿势3.1 为什么 MVVM 下动画会失控MVVM 是 WPF 的主流架构但它和动画之间有一个天然的矛盾点动画通常属于视图层而 MVVM 鼓励业务状态交给 ViewModel。如果你仍然在代码后置里写动画往往会破坏 MVVM 的分层。更麻烦的是很多开发者直接把动画绑定到属性上结果发现 UI 不停触发绑定更新动画频繁被打断出现“动画显示不全”“不断闪烁”的问题。就拿热搜词“wpf 数据绑定”来说绑定是双向的当动画修改了Opacity、Margin这类属性时如果它们同时绑定到 ViewModel 属性就会触发属性变更通知。这时如果 ViewModel 里的 setter 又更新其他属性就可能引起绑定循环。所以我的建议是动画操作的 UI 属性尽量不要和 ViewModel 绑定同一个依赖属性。动画负责临时性的视觉效果ViewModel 只负责业务数据视觉层的变化由代码或触发器完成而不是写进 VM。举个例子做搜索框提示当HasResults为 true 时结果面板应该淡入。你可以把HasResults绑到 Visibility但这样淡入动画就不好控制。更好的做法是ViewModel 暴露HasResults视图层通过DataTrigger触发界面动画。如果一个属性在 ViewModel 里变了界面上相关元素的动画就需要配合而不是让动画去驱动 ViewModel 属性。3.2 MVVM 下触发动画的几种模式DataTrigger、TriggerBehavior 与 Code-behind第一种模式是DataTrigger。在Style里定义触发器当某个绑定值变化时用Setter改一个辅助属性并用EnterActions/ExitActions添加 Storyboard。这里有个技巧EnterActions里的动画是“进入状态时播放”ExitActions是“离开状态时播放”。我一般先在控件模板里定义好 Storyboard然后在触发器里引用它们。第二种模式是使用行为Behavior比如Microsoft.Xaml.Behaviors.Wpf中的EventTriggerBehavior和DataTriggerBehavior。行为库允许你把动画放到 XAML 中不需要写大量代码后置同时又能响应事件或数据变更。我用得最多的是ChangePropertyAction和ControlStoryboardAction。注意行为库的命名空间WPF 用的是Microsoft.Xaml.Behaviors不要和 UWP 或旧版 Blend 混淆。第三种模式是代码后置在 View 的DataContextChanged或某个按钮事件里写很少量的代码去主动调用 ViewModel 的方法。我个人允许代码后置的存在但要限制在“纯视图逻辑”的范围内。比如点击按钮后动画播放需要根据当前滚动位置计算长度这属于视图逻辑放在 Code-behind 取数据比硬塞给 ViewModel 更干净。3.3 使用附加属性封装动画逻辑如果项目里的动画五花八门从淡入淡出到展开折叠都有我建议你封装一个“动画附加属性”。这样你的 XAML 可以写成下面这样Border local:AnimationBehavior.FadeInOnLoad500 local:AnimationBehavior.Offset40/附加属性在静态构造函数里注册然后在属性变更回调里创建并播放动画。这么做的好处是视图层只声明意图具体动画过程都集中在一个类里可维护性很高。我自己的AnimationBehavior类里包含FadeInOnLoad、ScaleInOnLoad、SlideInFromDirection等几十个静态属性和方法。后来这个类被团队其他人广泛使用大家都说好用。不过需要注意附加属性发生变更时会触发回调但同一个元素可能会被动态 Reset所以回调里要判断新旧值避免重复播放。此外如果你附加属性绑定到动画的属性比如要让动画目标值和某个数据上下文有关建议在回调里从element.DataContext中读取参数不要试图在 XAML 里加上复杂的绑定否则很容易绑定失效。4. 进阶主题3D 动画、Loading 动画与性能优化4.1 3D 动画看板到底怎么入门热搜词里有“wpf 实现3d 动画看板”这里我简单聊聊。WPF 内置 3D 能力基于Viewport3D、ModelVisual3D、PerspectiveCamera等类型。它不像 Unity 那样强大但做一些简单的模型展示、数据点旋转、广告面板效果够用了。一个最简单的 3D 旋转看板思路创建一个Viewport3D。添加一个ModelVisual3D其Content为Model3DGroup里面放GeometryModel3D。用MeshGeometry3D定义模型的三角面片。用DiffuseMaterial给模型上色甚至可以用VisualBrush把一个 WPF 控件画到模型表面。最后写一个RotateTransform3D用Rotation3D里的AxisAngleRotation3D来设置旋转轴和角度再用DoubleAnimation动态改角度。这里最坑的是坐标和三角形索引手动写容易翻车。我的经验是先用建模工具比如 Blender导出 OBJ 或 X 模型再用工具转换成 WPF 用的模型文件。如果只是做一个数据看板可以不用真实 3D 模型而是把几个TextBlock用ProjectionCamera投影到平面上配合动画让它们轮换视觉上也有 3D 效果。当然如果你用的是第三方图表库里边已经封装好了 3D 看板直接配置也更省事。性能上3D 场景尽量不要每帧更新大量模型尽量优化模型三角面数。我做过的看板最多用了 5000 个三角形在普通办公电脑上依然流畅。但设置Camera和Light一定要合理否则整个画面会过暗或过亮。不少人忘了加AmbientLight会导致模型基本看不清。4.2 高性能 Loading 动画的设计思路Loading 动画是桌面应用手里最低调的加分项。一个流畅的 loading 动画能大大缓解用户等待的焦虑。但很多人的 Loading 动画一跑CPU 占用率就飙升问题出在什么地方多半是Opacity动画和LayoutTransform动画用得太多或者大量使用DispatcherTimer去更新 UI。推荐的高性能 Loading 方式旋转动画用一个RotateTransform作用在RenderTransform上动画只改角度值不触发布局。透明度闪烁Opacity动画注意属性值变化也会触发渲染线程但比修改Visibility好在不引起布局。缩放心跳ScaleTransform用RenderTransformOrigin0.5,0.5让缩放围绕中心。如果做多个点的加载动画比如三个点依次变亮不要用三个独立Storyboard。我推荐用一个DoubleAnimationUsingKeyFrames配合固定间隔或者用ObjectAnimationUsingKeyFrames改变点的颜色。或者更简单用RepeatBehaviorForever的路径动画。另外要注意Loading 动画如果是在/Window里请把它放在一个Popup或者独立的Window上并保证关闭后所有动画已停止。我犯过的错误是窗口关闭后后台还有一个Forever的动画在跑导致进程无法退出查到原因是某个 Storyboard 没有Stop。4.3 动画性能常见问题动画显示不全、内存消耗大、卡顿“动画显示不全”这个热词很有意思。我遇到过几种情况元素在动画过程中被ClipToBounds裁剪导致显示不全或者动画设置了From和To却没有设置Duration导致动画一闪而过还有动画的TargetName在模板内找不到导致动画没效果。排查方式是先看输出窗口有没有绑定错误再检查Storyboard.TargetName是否写对了。如果动画目标是模板内部元素需要FindName获取到对应的具体实例不能直接写 XAML 名称。动画内存消耗大的一个原因是VisualBrush或BitmapCacheBrush被反复渲染。动画过程中如果每次刷新都让视觉树重新创建 Drawing内存自然爆。解决办法是尽量用RenderOptions.CachingHintCache把静态内容缓存下来只让变换层动。卡顿还有一个容易被忽略的因素动画期间如果同时有大数据集合绑定到 ListBox发现 UI 线程被集合变化通知塞爆。动画本身并不卡卡的是集合频繁通知导致布局重算。我的建议是动画播放期间冻结集合通知或者用INotifyCollectionChanged的批量通知模式。4.4 调试动画的实战技巧如何看动画是否在跑调试动画有时很玄学。我的习惯是用 WinDbg 或 Visual Studio 的实时可视化树来检查当前动画状态。但更实用的方法是加日志在动画的Completed事件里写一条Debug.WriteLine看看它有没有触发。另一个技巧是利用Storyboard.GetCurrentState和Storyboard.GetCurrentTime来获取动画当前状态。这两个 API 对排查“为什么不动”非常有用。还有一个容易忽略的点如果你在Style里给元素设置了Opacity在动画里又去改Opacity动画会覆盖 Style 里的值。这是正常行为但如果你在动画结束后把Opacity的FillBehavior设为Stop它会回到 Style 里的值。这个逻辑和 WPF 依赖属性优先级完全一致想清楚了就不会出 bug。5. 实战把上述知识点揉进一个动态仪表盘5.1 场景与界面布局光说理论太虚我拿一个实际做过的小项目来收尾一个股票/指标监控的简易仪表盘。界面由一个背景面板、一个指针和一组数值文字组成。核心需求是数据更新时指针平滑转动到新角度数值渐变并用颜色高亮变化方向。界面布局简单使用Canvas作为根面板指针是一个Polygon数值用TextBlock。为了美观加了一个半透明渐变背景。5.2 数据绑定与动画联动我的 ViewModel 里有一个double _value属性更新时会触发PropertyChanged。视图里一个转换器把数值映射为指针角度。然后在PropertyChanged事件里判断数值变化后调用自定义动画函数private void OnValueChanged(double oldValue, double newValue) { var angle ValueToAngle(newValue); var animation new DoubleAnimation(CurrentAngle, angle, TimeSpan.FromMilliseconds(600)) { EasingFunction new CubicEase { EasingMode EasingMode.EaseOut } }; _pointerRotation.BeginAnimation(RotateTransform.AngleProperty, animation); }这里为了直观我直接操作RotateTransform的AngleProperty。如果项目走 MVVM也可以创建一个Angle属性在 VM并通过INotifyPropertyChanged触发角度绑定。但注意用绑定后动画每次都会重设“当前值”所以动画体验不如直接控制 Transform。我的实际建议是动画控制权保留在视图层VM 只负责提供数值。数值文本的颜色变化用ColorAnimation从正常色过渡到升高/降低的颜色再自动恢复。具体做法是给TextBlock.Foreground绑定一个SolidColorBrush实例然后用BeginAnimation改变它的Color。5.3 加载与交互细节为了让仪表盘在启动时不显得突兀我给整个面板施加一个“载入动画”让指针从 -45 度转到初始角度同时透明度从 0 到 1背景面板有一个轻微的缩放动画。这样用户一看到这个界面就感觉到了“精心设计”不必解释太多。然后给鼠标悬浮时的值卡片加一个轻微的TranslateTransform上浮效果并配一个阴影提升交互反馈。这些动画全部用RenderTransform实现不干扰布局性能开销非常小。5.4 性能优化与发布前的自检发布前我会检查三件事动画运行期间 CPU 是否超过 20%在普通笔记本上。如果超过优先检查是不是有背景动画没停。动画数量是否过多。很多动画同时播放时会争抢 UI 线程我会把不必要的动画移出视觉树或用Opacity0代替。内存是否有持续增长。打开性能监视器观察反复进入退出页面后内存是否回落。如果只升不降就要检查是否有 Storyboard 引用未释放。另外程序的app.config通常不需要特别配置和动画有关的东西但如果你用到软件渲染或特定显卡驱动可能需要在app.config里设置渲染模式不过我基本没碰过默认选项已经足够。6. 平时最容易忽略的坑动画与布局、资源加载的边界6.1 动画改布局属性时的重算风险前面提到过Margin、Width、Height这类属性会触发布局系统。如果动画持续改这些属性会导致布局不断重算性能变差。比如做一个展开动画如果直接用Height从 0 变到 200中间每一次变化都会触发Measure/Arrange。小规模没问题但如果你在一个ListBox里有几十个展开项就会卡顿。更好的方案是给内容套一个Clip或ScaleTransform或者先用默认展开的尺寸再用LayoutTransform缩放。当然LayoutTransform也会触发布局所以更推荐用RenderTransform加Clip。但裁剪又可能导致“动画显示不全”。最好的方案其实是用Expander控件自带的展开动画或者使用Grid的RowDefinition.Height动画再配合SizeChanged事件调整内容尺寸。这里没有银弹需要根据场景取舍。6.2 资源加载与动画初始状态如果你的动画在Loaded事件之前就Begin了大概率不会有效果因为此时视觉树还没准备好。所以很多人用EventTrigger RoutedEventLoaded确保动画在加载完成后播放。但如果你的控件在虚拟化列表里Loaded可能会被不断触发导致动画反复播放。这时候要做一个“是否已经播放过”的判断。我的做法是给附加属性加一个HasPlayed标记或者直接使用Loaded后Unloaded再停止动画。虚拟化列表里头元素被回收后再加载如果动画还在运行可能会产生错乱视觉效果。记得在Unloaded时停止所有 Storyboard。6.3 Edge case无焦点、窗口最小化、高 DPI 缩放下的动画窗口最小化后WPF 会停止调度动画时间因为渲染循环不再产生帧。等窗口恢复时动画会从停止时继续这是正常的。但如果你用Timer做动画窗口最小化时 Timer 依然在后台跑会导致计时结束突然跳到终值。所以尽量用 WPF 的时间系统不要自己用DispatcherTimer刷新。高 DPI 下动画也可能出现模糊或位置偏差。建议在app.manifest或启动代码里设置PerMonitorV2的 DPI 感知。实际测试中DPI 对动画本身影响不大但如果你用VisualBrush做材质可能会因为分辨率不同出现边缘模糊。必要时可以用RenderOptions.BitmapScalingModeHighQuality缓解。7. 我对 WPF 动画的一些个人心得最后分享一点我在实际工作中沉淀下来的经验。不要为了动画而动画每一个动画都应该服务用户理解数据变化时用高亮提示状态切换时用平滑过渡层级变化时用轻微位移。动画时长控制在 200ms600ms 之间过长会拖慢操作节奏过短又感觉不到。如果你不确定某个动画是否合适把时长调长一倍观察用户是否在等待动画结束如果用户明显在等说明动画已经影响效率了。我还发现WPF 动画团队留下的许多内置能力被严重低估。比如HandoffBehavior的SnapshotAndReplace和KeyFrame的组合可以实现很多复杂的交互动画。不要一上来就引入第三方动画库先把 WPF 自带工具吃透通常性能更好也更稳定。如果你要把这套经验进一步扩展可以从自定义控件开始把动画封装到控件的内部状态机里。这样无论界面怎么改动画都能保持一致。我曾经花了一周时间把一张旧项目里散落的 300 多处动画全部重构到一个自研动画框架里后续两个月几乎没有再出过动画相关的 bug。所以多花时间在设计动画结构上远比单个动画效果重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑