
单个动画这四个字听起来不起眼但真正在 Qt Creator 里做过界面动效的人都清楚所有复杂的动效最后都得拆回到一个 QPropertyAnimation、一个 NumberAnimation 这样的最小单元上。你把单个动画吃透了串行动画、并行动画、状态机驱动的转场都只是排列组合的事。我这次就把 Qt Creator 里做单个动画这件事完整拆一遍从框架选型到代码落地从参数计算到踩坑排查Widgets 和 QML 两条路都走一遍看完你应该能直接抄着改。这套内容适合刚接触 Qt 动画、被动画写了但不生效折磨过的同学也适合已经在用 Qt 做产品界面、想把动效手感调得更细一点的开发者。Qt Creator 只是一个载体文中涉及的类、属性、参数在别的编辑器里一样成立。1. Qt 动画体系的全景与单个动画的定位Qt 的动画模块QtCore 里的 QAbstractAnimation 家族加上 QtQuick 里的 QML 动画类型看起来种类很多但拆开看只有三层最底层是时间轴驱动中间层是属性插值最上层是动画组合与状态绑定。单个动画正好卡在中间层——它自己管一条时间轴负责把一个属性从 A 值平滑推到 B 值不涉及组合逻辑。先把这个定位想明白后面选类就不会乱。1.1 为什么先啃 QPropertyAnimation 而不是别的类QPropertyAnimation 是 Qt 动画体系里最通用的一把刀。它的核心能力只有一句话给任意一个继承自 QObject、并且用 Q_PROPERTY 声明过可写属性的对象做属性值的插值动画。这意味着它能动 QWidget 的 pos、geometry、size能动自定义控件的自定义属性也能动 QGraphicsEffect 的 opacity。你学会它之后换目标换属性就行不用换思路。相比之下QVariantAnimation 只产出值、不绑定对象适合做数值计算类的动画比如让一个进度值从 0 跑到 100然后自己在 valueChanged 信号里手动更新好几个控件。QVariantAnimation 灵活但需要你手写值怎么落到界面上这段胶水代码新手容易在这里写出性能问题。我一般的建议是能用 QPropertyAnimation 就直接用只有当你要驱动的东西不是一个可写属性、或者一次要驱动多个互相关联的数值时才退到 QVariantAnimation。提示QPropertyAnimation 的目标属性必须是 QVariant 能装下的类型并且类型要和属性的 setter 匹配。你给一个 QPoint 属性塞 int运行时不会报错但动画会静默失效这个坑后面还会细说。至于 QSequentialAnimationGroup 和 QParallelAnimationGroup它们本身不产生任何动画效果只是把若干单个动画按顺序或同时播放。你连单个动画的时序都没摸清的时候去用组合类出了问题根本定位不到是哪一段的锅。所以先单后组顺序不能反。1.2 Widgets 和 QML 两条路线先想清楚你在哪边Qt Creator 里做动画有两条完全不同的路线选错了会走很多弯路。第一条是传统 Widgets 路线动画能力由 QtCore 提供你面对的是 C 类动的是 QWidget 的各种属性。第二条是 QML 路线动画是语言内建的一等公民你面对的是声明式语法动的是 QML 元素的属性。判断标准很直接你的项目 UI 已经用 .ui 文件加 QWidget 搭完了那就走 Widgets 路线用 QPropertyAnimation不要去混 QML两套渲染体系混用会让打包体积和调试复杂度都翻上去。如果你是从零开始做一个偏展示、偏动效的产品界面或者要做流畅的列表滚动、卡片切换那 QML 天生更合适NumberAnimation 加 Behavior 能少写一大坨 C 代码。我自己在实际项目里的取舍是工具类、工业上位机这类以功能为主的界面留在 Widgets 里用 QPropertyAnimation 做点缀式动效面向终端用户、动效占比高的界面直接上 QML。两条路线不要在一个项目里反复横跳。1.3 单个动画的四个核心要素任何一个单个动画本质上都由四样东西定义动谁target、动什么property、动多久duration、怎么动缓动曲线。前两个决定动画能不能生效后两个决定动画好不好看。新手最常见的误区是把注意力全放在 duration 和缓动上结果动画不生效回头查半天才发现是 property 名字写错了或者目标对象在动画开始前就被析构了。我的习惯是写动画之前先在纸上把动谁、动什么这两项确认一遍确认目标对象在整个动画生命周期内活着、属性确实可写再去调手感的参数。这个顺序反过来你会浪费大量时间在无效调试上。2. QPropertyAnimation 关键参数逐个拆解这一节把单个动画涉及的核心 API 一个个过一遍每个参数我都说清楚它控制什么、设错会怎样。这些都是官方文档里写了但没告诉你为什么的部分。2.1 构造函数与目标属性的绑定方式QPropertyAnimation 最常用的构造函数有三个参数目标对象指针、属性名QByteArray、父对象指针。属性名传字符串比如pos、geometry、opacity运行时通过元对象系统去查找。这个设计带来一个隐患属性名是字符串编译器不检查写错了要到运行时才发现而且默认情况下不会给你一句明确报错。QPropertyAnimation *anim new QPropertyAnimation(button, pos, this);上面这行如果button是空指针或者pos拼成了poss你得到的是一次沉默的失败动画对象创建成功、start() 也返回了但界面上什么都不会动。所以我强烈建议在开发阶段加上一句校验用元对象系统自己查一遍属性是否存在const QMetaObject *mo button-metaObject(); int idx mo-indexOfProperty(pos); if (idx 0) { qWarning() 属性不存在动画不会生效; }这段代码我几乎在每个动画封装类里都保留着尤其在封装通用动效工具类的时候多这一行能省掉大量为什么有的控件能动有的不能动的排查时间。另外构造函数里的父对象一定要给。给了父对象动画对象就挂进了 Qt 的对象树目标对象销毁时能连带清理减少悬空指针的风险。很多教程里写new QPropertyAnimation(target, pos)不给父对象例子能跑但放进真实项目就是内存泄漏点。2.2 duration、startValue、endValue 三者的关系duration 是动画总时长单位毫秒默认值是 250。这个默认值其实挺尴尬对于按钮反馈类的微动效250 毫秒略长对于卡片滑动这类需要存在感的动画250 毫秒又太快容易让人觉得闪了一下。我的经验区间是微反馈 100 到 150 毫秒常规元素进场 300 到 450 毫秒大区块位移或全屏转场 500 到 800 毫秒。超过 800 毫秒用户就会开始觉得界面卡。startValue 和 endValue 是可选的。如果你不设 startValueQt 会在 start() 被调用的那一刻去读目标属性的当前值拿它当起点。这个行为大多数时候很方便但有一个典型陷阱动画还没播完的时候你又调了一次 start()此时属性的当前值是动画进行到一半的中间值新动画就会从半路开始视觉上会出现卡顿一下再走的怪异感。// 不推荐连续触发时起点会漂移 anim-setEndValue(QPoint(120, 80)); // 推荐起点写死反复触发也稳定 anim-setStartValue(QPoint(-120, 80)); anim-setEndValue(QPoint(120, 80));如果你确实想让起点跟随当前值比如做跟随鼠标的拖拽回弹那我建议在调 start() 之前先调用stop()让动画回到确定状态再重新开始。这个细节在我早期做侧边栏抽屉的时候踩过一次快速连点按钮抽屉位置会一点点往右偏查了两小时才定位到这里。endValue 支持 QVariant 的各种类型但必须和属性类型严格对应。QWidget 的 pos 是 QPoint你给它 QPointF动画会失效geometry 是 QRect给它 QRectF 同样失效。类型不匹配时 Qt 不会抛异常只是 interpolated 出来的值设置不进去表现就是动画像是跑了但界面没动。我调试这类问题的土办法是接上 valueChanged 信号打印一下看值有没有在变connect(anim, QPropertyAnimation::valueChanged, [](const QVariant v) { qDebug() 当前插值: v; });值在变但界面不动就是类型或属性名的问题值都不变那就是目标对象或者生命周期的问题。2.3 缓动曲线手感差异的真正来源缓动曲线QEasingCurve决定属性值随时间变化的映射关系。线性曲线是匀速但人眼对匀速运动的感知是机械、廉价所以真实的 UI 动效几乎都会用带加减速的曲线。QEasingCurve 内置了四十多种曲线名字的规律是方向前缀In / Out / InOut 类型Quad / Cubic / Quart / Quint / Sine / Expo / Circ / Back / Elastic / Bounce。理解前缀的物理含义比背名字有用。In 表示慢起步快结束适合元素离场Out 表示快起步慢结束适合元素进场——因为人眼对运动结束时的减速更敏感缓收尾会让动画显得稳InOut 两端都柔和适合在场内移动的元素比如卡片左右切换、抽屉开合。我列一张常用曲线的对照表这张表是我自己在项目里反复用下来觉得最实用的组合| 曲线名 | 运动特征 | 我实际会用的场景 | | Linear | 完全匀速 | 进度条填充、无限循环的走马灯 | | OutQuad | 起步稍快收尾柔和 | 通用元素进场兼容性最好 | | OutCubic | 收尾感更强 | 按钮、卡片进场比 OutQuad 更有质感 | | InOutCubic | 两端都柔和 | 抽屉、面板开合卡片左右切换 | | OutBack | 末端轻微过冲后回落 | 强调新元素出现比如提示气泡 | | OutBounce | 像球落地弹几下 | 游戏化界面、趣味提示 | | OutElastic | 弹性震荡 | 玩具类应用正式产品慎用 | | InCubic | 起步慢、末段猛 | 元素离场配合 OutCubic 使用 |选曲线有个偷懒但好用的原则进场用 Out 系离场用 In 系场内位移用 InOut 系需要活泼加 Back 或 Bounce需要昂贵感用 Expo 或 Quint。按这个原则挑基本不会出错。2.4 循环、方向与动画状态单个动画也有几个容易被忽略的控制项。setLoopCount(-1)让动画无限循环做呼吸灯、加载指示器时很有用setDirection(QAbstractAnimation::Backward)让动画反向播放配合setLoopCount(2)可以做出去再回来的效果不用写两条动画。setCurrentTime(ms)能把动画直接定位到某个时间点做动画调试或者帧对齐的时候非常省事。动画的状态通过state()查询也会通过stateChanged信号发出。状态有 Stopped、Paused、Running 三种。我经常用 stateChanged 来做简单的串联第一个动画进入 Stopped 且方向是正向时启动第二个。当然正规做法是用 QSequentialAnimationGroup但临时串一两条的时候信号连接更直接。connect(anim, QAbstractAnimation::stateChanged, this, [this](QAbstractAnimation::State now, QAbstractAnimation::State old) { if (old QAbstractAnimation::Running now QAbstractAnimation::Stopped) { // 第一个动画播完了接第二个 } });3. Widgets 路线上手做一个按钮滑入的完整动画理论说够了直接上代码。这个例子的目标是一个按钮从窗口左侧外部滑入到指定位置同时淡入。做成这样是因为它同时覆盖了位移动画和透明度动画又保持在单个动画的范畴内——注意是两条独立的单个动画同时跑不是动画组。3.1 工程准备与控件布局的坑先在 Qt Creator 里新建一个 Widgets Application基类选 QWidget不勾选生成 .ui 文件。为什么不勾因为 .ui 文件生成的界面会挂布局管理器而布局管理器会在窗口尺寸变化时重新计算子控件位置直接把你的 pos 动画结果覆盖掉。这是 Widgets 做位移动画第一大坑。注意只要子控件被布局管理器接管你手动 move 或者用 QPropertyAnimation 动 pos效果都会在布局重算的瞬间被打回原形。做位移动画的控件要么不放进布局要么动画期间临时解除布局管理。那实际项目里怎么兼顾布局和动画两个方向。第一把要动的控件放进一个不带布局的容器里容器本身放进布局动画只动容器内部的坐标。第二动画结束后再把控件交给布局动画期间手动控制。第一种更干净我一般用第一种。3.2 完整代码实现先把头文件和实现写好我贴的是可以直接编译的版本。// slidewidget.h #ifndef SLIDEWIDGET_H #define SLIDEWIDGET_H #include QWidget #include QPoint class QPushButton; class QPropertyAnimation; class QGraphicsOpacityEffect; class SlideWidget : public QWidget { Q_OBJECT public: explicit SlideWidget(QWidget *parent nullptr); void playIn(); private: QPushButton *m_btn nullptr; QPropertyAnimation *m_slideAnim nullptr; QPropertyAnimation *m_fadeAnim nullptr; QGraphicsOpacityEffect *m_effect nullptr; }; #endif// slidewidget.cpp #include slidewidget.h #include QPushButton #include QPropertyAnimation #include QGraphicsOpacityEffect #include QEasingCurve #include QDebug SlideWidget::SlideWidget(QWidget *parent) : QWidget(parent) { setFixedSize(420, 220); m_btn new QPushButton(QStringLiteral(确定), this); m_btn-setFixedSize(120, 40); m_btn-move(-120, 90); // 起点完全在可视区外避免动画开始前被看到 // 校验属性是否存在开发期保留 Q_ASSERT(m_btn-metaObject()-indexOfProperty(pos) 0); // 位移动画 m_slideAnim new QPropertyAnimation(m_btn, pos, this); m_slideAnim-setDuration(416); m_slideAnim-setStartValue(QPoint(-120, 90)); m_slideAnim-setEndValue(QPoint(150, 90)); m_slideAnim-setEasingCurve(QEasingCurve::OutCubic); // 透明度动画QWidget 没有 opacity 属性靠 QGraphicsOpacityEffect 提供 m_effect new QGraphicsOpacityEffect(m_btn); m_effect-setOpacity(0.0); m_btn-setGraphicsEffect(m_effect); m_fadeAnim new QPropertyAnimation(m_effect, opacity, this); m_fadeAnim-setDuration(416); m_fadeAnim-setStartValue(0.0); m_fadeAnim-setEndValue(1.0); m_fadeAnim-setEasingCurve(QEasingCurve::InOutQuad); } void SlideWidget::playIn() { if (m_slideAnim-state() QAbstractAnimation::Running) return; // 防止连点导致起点漂移 m_slideAnim-start(); m_fadeAnim-start(); }// main.cpp #include slidewidget.h #include QApplication #include QTimer int main(int argc, char *argv[]) { QApplication a(argc, argv); SlideWidget w; w.show(); QTimer::singleShot(100, w, SlideWidget::playIn); return a.exec(); }这段代码有几个点值得展开说。第一duration 我设的是 416 而不是 400。原因在后面参数计算那节会详细讲简单说就是让动画时长和 Qt 动画驱动的刷新节拍对齐减少末帧抖动。第二透明度动画没有作用在按钮上而是作用在 QGraphicsOpacityEffect 上。QWidget 本身没有 opacity 这个属性你想让整个控件半透明只能套一层图形效果。这里有个坑给控件设置 graphicsEffect 之后这个控件的渲染会走离屏路径性能比直接绘制略差。如果一个界面上有成百上千个控件都挂效果帧率会掉。做几个、几十个元素的进场淡入完全没问题做大量粒子特效就别用这条路。第三按钮的初始位置是(-120, 90)不是为了效果是为了防止动画开始前按钮在原点闪一下。很多时候你看到的动画前闪一下根因就是控件初始位置没设好构造完到动画开始之间有那么几毫秒它待在默认位置。第四playIn 里加了一层状态判断。用户连点触发按钮时如果动画还在跑就忽略新的请求这样起点永远是写死的那个值动画稳定。如果你想要打断并立即重播的行为那就先 stop 再改 startValue 为当前值再 start。3.3 参数计算duration 到底该设多少这部分是我觉得最值得单独讲的内容大多数教程都跳过。Qt 的动画驱动基于一个内部定时器默认间隔约 16 毫秒对应约 60 帧每秒。你设的 duration 如果不是 16 的整数倍最后一帧的间隔会和其他帧不一样快速位移的动画在收尾时可能有一帧的轻微卡顿感。所以我会把 duration 尽量取 16 的整数倍| 目标时长 | 16 的整数倍取值 | 实际秒数 | | 150 ms 左右 | 160 | 0.160 s | | 250 ms 左右 | 256 | 0.256 s | | 300 ms 左右 | 304 | 0.304 s | | 400 ms 左右 | 416 | 0.416 s | | 500 ms 左右 | 512 | 0.512 s | | 700 ms 左右 | 704 | 0.704 s |另外位移距离也影响 duration 的合理值。同样是 400 毫秒移动 50 像素会显得慢吞吞移动 800 像素又会显得飞快。有个粗略的经验公式可以参考基础时长 200 毫秒加上每移动 100 像素补 40 到 60 毫秒上限压在 800 毫秒以内。比如移动 300 像素大约 200 3×50 350 毫秒往上取到 16 的整数倍就是 352。这个公式不是什么科学定律但它比拍脑袋设 1000 毫秒靠谱得多我拿它给几十个界面调过基本都落在合理区间。缓动曲线也会影响 perceived 速度。OutCubic 前半段跑得快视觉上比同 duration 的 OutQuad 更利落所以用 OutCubic 时 duration 可以比 OutQuad 略短一点整体节奏才一致。这个只能靠对比着看我一般会把两三版参数放在同一个界面上来回切眼睛给出的答案比数字准。4. QML 路线单个动画的声明式写法如果你的界面是 QML 写的那单个动画可以写得更短。QML 里做属性动画主要有两种形态显式的动画对象和挂在属性上的 Behavior。这两种的手感差别不小选错了后面改起来很麻烦。4.1 用 NumberAnimation / PropertyAnimation 显式控制显式动画的意思是动画对象存在什么时候播由你决定调start()或把running设成 true。import QtQuick 2.15 import QtQuick.Window 2.15 Window { width: 480 height: 320 visible: true color: #1e1e1e Rectangle { id: card width: 200 height: 120 radius: 10 color: #3a7afe x: -220 y: 100 NumberAnimation on x { id: slideIn from: -220 to: 140 duration: 416 easing.type: Easing.OutCubic running: false } MouseArea { anchors.fill: parent onClicked: slideIn.start() } } }NumberAnimation on x这种写法叫属性值源它已经把动画绑定到 x 上了你只需要控制 running。如果你想在多个目标之间复用一条动画那就用独立的 PropertyAnimation通过 target 和 property 指定PropertyAnimation { id: genericMove property: x duration: 416 easing.type: Easing.OutCubic } // 使用时 genericMove.target cardA; genericMove.from -220; genericMove.to 140; genericMove.start();复用写法在列表、卡片组这种同构元素多的场景里特别省事。注意 from 和 to 必须在 start() 之前设好如果在动画运行中改 from行为会有点微妙——Qt 会用新的 from 重新计算插值视觉上可能跳一下。4.2 Behavior on 与显式动画的区别什么时候该用哪个Behavior 是 QML 里最优雅的地方你只在属性上挂一次之后这个属性任何变化都自动带动画。Rectangle { id: card width: 200 height: 120 color: #3a7afe x: expanded ? 140 : -220 Behavior on x { NumberAnimation { duration: 416; easing.type: Easing.OutCubic } } }这样你只要改expanded卡片就会平滑地滑过去。代码量比显式动画少一半可读性也好。但 Behavior 有两个需要注意的地方。第一它是全局生效的属性只要变就动画包括你想让它瞬间变的时候。想临时关掉可以设enabled: false。第二初次绑定时如果属性的初始值是通过绑定表达式算出来的Behavior 不会对初始化赋值触发动画——它只对赋值改变生效。这个特性大部分时候是好事不会一开界面就一堆元素乱飞但如果你就是想让它一进来就滑那得用显式动画在 Component.onCompleted 里手动 start。我的选择习惯是这样的属性值由状态驱动、来回切换的场景用 Behavior代码干净只在特定时机播一次的进场动效用显式动画控制精确。混着用也行但同一个属性上挂了 Behavior 又手动跑显式动画会出现两者抢属性的情况动画会互相打断看起来一卡一卡的。这个坑我吃过后来就定下规矩同一个属性只留一种动画机制。4.3 QML 里做淡入的写法QML 元素的 opacity 是原生属性不需要像 Widgets 那样套图形效果直接动就行。Rectangle { id: panel opacity: 0 y: -60 ParallelAnimation { id: entrance NumberAnimation { target: panel; property: opacity; from: 0; to: 1; duration: 416 } NumberAnimation { target: panel; property: y; from: -60; to: 20; duration: 416; easing.type: Easing.OutCubic } } }这里用了 ParallelAnimation 同时跑两条属性动画本质还是单个动画的并排组合。QML 里 opacity 动画的性能比 Widgets 好很多因为 QML 场景图在渲染线程里处理不用走离屏渲染。所以 QML 做大量元素的淡入淡出比 Widgets 有天然优势这也是我前面说动效占比高的界面选 QML的原因之一。5. 动画生命周期与内存管理这一节讲的是动画为什么不生效里最隐蔽的一类原因动画对象本身活得不合适。QPropertyAnimation 是 QObject 子类它的生存期必须覆盖整个动画过程这是硬性要求。5.1 局部变量陷阱与 DeleteWhenStopped很多人第一次写动画会这样写void MyWidget::slideOut() { QPropertyAnimation anim(btn, pos); // 栈对象 anim.setDuration(300); anim.setEndValue(QPoint(-120, 90)); anim.start(); } // 函数返回anim 析构动画直接没了栈上创建的动画对象在函数返回时析构动画被强制终止界面上什么都不会发生。这类问题很难查因为编译没问题、运行也不报错。正确做法是用堆对象并且明确它的所有权。两种常用方式// 方式一父对象挂给 this长期持有反复复用 m_anim new QPropertyAnimation(btn, pos, this); // 方式二一次性动画播完自动销毁 auto *anim new QPropertyAnimation(btn, pos, this); // ... 设置参数 ... anim-start(QAbstractAnimation::DeleteWhenStopped);方式一适合会反复触发的动效动画对象复用省去反复 new 的开销也不用担心内存。方式二适合一次性的进场、退场动画代码短不会留下这动画到底谁在管的疑问。我只在确定这条动画不会再播第二次时才用方式二因为它播完就没了你要是之后再调 start()就作用在一个已删除的对象上。注意使用 DeleteWhenStopped 时不要在动画结束后通过保存的指针去访问它也不要再调 stop()。这类崩溃在 Release 版本里特别难复现Debug 下有时又正常因为内存没被立刻覆盖。5.2 目标对象先于动画被销毁反过来的情况更危险动画对象活着目标对象没了。Qt 在这里做了保护QPropertyAnimation 内部是通过 QPointer 弱引用跟踪目标对象的目标销毁后动画会自动停止不会直接崩。但如果你在动画停止后还去访问那个已经删除的目标指针那就是你自己的问题了。我处理这个问题的方式是所有动画对象都设父对象父对象选择动画生命周期的宿主而不是目标对象。比如一个面板的进场动画宿主设成这个面板所属的窗口而不是面板本身。这样面板被销毁时窗口还在动画对象安安静静地停在那儿不会有悬空。5.3 动画结束后的状态复位有个很常见的需求动画播完后把目标对象恢复到某个确定状态或者把控件重新交还给布局管理。这时候接 finished 信号connect(m_slideAnim, QAbstractAnimation::finished, this, [this]() { // 动画结束做后续处理 m_btn-setEnabled(true); });finished 和 stateChanged(Stopped) 的区别在于finished 只在动画自然播放到终点时发出手动调 stop() 触发的停止不会发 finished但会发 stateChanged。这个差别在做被打断逻辑时很重要。比如抽屉打开动画播到一半用户又点了关闭此时 stop 旧动画、启动反向动画你不希望旧动画的 finished 回调去执行打开完成的后续逻辑用 finished 而不是 stateChanged 就天然规避了这个问题。6. 常见问题与排查技巧实录前面零散提了一些坑这一节集中整理成速查表附上我的排查顺序。遇到动画不动按这个顺序走一遍基本上十分钟内能定位。6.1 动画完全不动五步排查法第一步确认目标对象非空。空指针是 QPropertyAnimation 最温和的失败方式什么都不做也不报错。第二步确认属性名存在且可写。用metaObject()-indexOfProperty()校验或者接 valueChanged 信号看值在不在变。值在变但界面不动就是属性类型不匹配或者目标对象不是你以为的那个对象。第三步确认动画对象在函数返回后还活着。检查是栈对象还是堆对象有没有父对象有没有误用 DeleteWhenStopped 之后再次使用。第四步确认 startValue 和 endValue 不是同一个值。端点相同时动画会正常跑完 300 毫秒但界面上没有任何变化这个特别容易被当成动画失效。我遇到过有人写setEndValue(btn-pos())结果端点就是当前值。第五步确认目标控件确实可见且没有被布局管理器在动画过程中重新摆放。控件 hide 状态下动画照跑只是你看不见布局接管的情况下你看到的画面是布局算出来的不是动画设的。6.2 常见问题速查表| 现象 | 最可能的原因 | 处理方式 | | 动画完全不生效无任何报错 | 属性名拼写错误 | 用 indexOfProperty 校验 | | 值在变但界面不动 | 属性类型与 QVariant 不匹配 | 检查 pos/geometry 的类型 | | 动画播完弹回原位 | 布局管理器覆盖了手动位置 | 动画控件移出布局或用容器隔离 | | 动画一闪而过 | duration 太短或未设 | 检查是否误用了默认 250 | | 连点后位置偏移 | 起点被读成中间值 | 写死 startValue或在 start 前判状态 | | 透明度动画无效 | 动的是 QWidget 本身 | 改用 QGraphicsOpacityEffect | | 动画结束后崩溃 | 访问了已销毁的动画或目标 | 检查父对象设置与 DeleteWhenStopped | | 动画看起来卡顿 | 主线程被阻塞 | 检查动画期间有没有同步 IO 或死循环 | | 动画期间窗口抖动 | 位移引起布局重算 | 用容器隔离或动画期间禁掉更新 |6.3 动画卡顿的三类根因卡顿的第一类原因是主线程被占住。Qt 的动画在事件循环里推进如果你在动画期间跑了一段耗时几百毫秒的同步计算、读了网络、查了大文件动画就会停在那一帧恢复后直接跳到正确时间点视觉上就是一顿。解决办法是把耗时逻辑挪到工作线程或者用 QTimer::singleShot 把后续逻辑推到动画之后再执行。第二类原因是布局重算。动画改变控件尺寸或位置时如果触发了父级布局重新计算而且布局里有大量子控件每帧重算的开销会很明显。我在做复杂面板展开动画时遇到过把内部控件的布局在动画期间设成setUpdatesEnabled(false)动画结束后再恢复帧率立刻稳了。第三类原因是过度绘制。给控件挂图形效果、加阴影、半透明叠加都会让绘制走更贵的路径。一个界面上同时跑十几条带动效的透明度动画比跑十几条位移动画要重得多。做分层淡入的列表动效时最好错开时间不要所有元素同一帧开始。6.4 调试动画的实用手段我调试动画有几个固定手段。第一是setCurrentTime()把动画定位到特定时间点逐帧检查布局对不对。第二是临时代码里把 duration 放大到 3000 毫秒动画慢放观察中间状态有没有跳变。第三是接 valueChanged 打印插值确认数值曲线是不是你预期的。还有一个很实用的技巧暂时给目标控件加一个明显的背景色动画是不是真的在动一眼就能看出来比盯着一个白底控件猜快得多。这个土办法我用得比任何调试器都多。7. 我在这件事上的一些实际体会做动画这几年最大的感受是参数比代码重要。同样一段 QPropertyAnimation 代码duration 从 250 改成 416缓动从 Linear 换成 OutCubic观感能差出一个档次而代码是一模一样的。所以我在项目里会单独留一个动效参数的文件把所有 duration、缓动曲线、位移量集中定义成常量改的时候全局生效不用满项目搜数字。第二个体会是克制。单个动画做起来太容易了容易到一不小心就给每个元素都加一个进场动画最后整个界面花里胡哨用户找不到重点。我的判断标准是这个动画是否帮助用户理解什么变了、从哪来、到哪去。帮助理解的留纯粹装饰的砍。这条标准让我删掉过很多自己写的动画。第三个体会是关于打断的处理。用户手速比你想的快得多动画还没播完就又操作是常态。所有超过 300 毫秒的动画我都会在触发处加一层进行中则忽略或者进行中则先停再重播的判断。默认选忽略只有明确希望快速重播的地方才选打断。这个判断加不加是看起来能用和用起来舒服的分界线。最后一个小技巧做界面对比的时候我会把两版参数分别放在两个并排的窗口里同时播眼睛瞬间就能分辨出哪版手感好。单看一版动画还行和很好的差别其实感觉不出来但放一起就非常明显。这个方法比调参数时反复猜有效得多。如果你后面要把单个动画升级成动画组或者用 QState 和动画对接做状态切换那都是在这套基础之上加一层组合逻辑核心还是这里讲的四条动谁、动什么、动多久、怎么动。把这一层焊牢上面怎么搭都稳。