1. Qt Creator 只是编辑器,真正让动画动起来的是 Qt 的框架
刚接触 Qt 的人经常会有个误区:以为"Qt Creator 实现动画效果"是指在 IDE 里点几下就能给控件加动效。我自己最开始也是这么理解的,翻了半天 Qt Creator 的菜单栏,找所谓的"动画面板",结果什么也没找到。实际上 Qt Creator 只是写代码的集成开发环境,真正干活的是 Qt 里的动画框架(Qt Animation Framework),核心类就是QPropertyAnimation。你在 Qt Creator 里做的所有事情,无非是敲代码、编译、调试、看效果,动画本身跟这个编辑器没有半毛钱关系。换个 IDE 用同样的代码,动画一样能跑。
所以这篇内容讲的是:在一个标准的 Qt Widgets 或 Qt Quick 工程里,怎么用单个动画把一个控件从 A 状态平滑推到 B 状态。重点放在"单个"这两个字上——不做动画组、不做状态机、不搞多属性联动,就是把一个动画从创建到收尾的完整链路走一遍。因为绝大多数动画翻车现场,都发生在最基础的单动画环节:属性名传错、对象生命周期搞乱、缓动曲线选得不合适、中途重复触发导致位置打架。这些坑踩透了,后面再上QSequentialAnimationGroup才会稳。
内容适合两类人看:一类是刚学完 Qt 基础控件、想给自己的小工具加点动效的新手;另一类是写了几年业务代码、动画全靠QTimer手搓、想换成标准做法的老手。我会把每一步选择的理由讲透,而不只是贴一段能跑的代码——因为找代码谁都会,知道为什么这么写才是省时间的地方。
1.1 一个常见误解:把 IDE 当成动画引擎
Qt Creator 提供的能力里,跟"看动画"沾边的其实只有几个地方。一是调试器,你可以在动画运行过程中查看动画对象的状态、当前的currentTime()和currentValue();二是 QML Profiler,用来分析 QML 动画的帧耗时和绑定开销;三是 Qt Quick Designer,能在编辑期预览部分 QML 动画效果,但预览能力和精确度都比较有限,复杂动效还是得实际运行看。至于 C++ Widgets 这边,Qt Creator 基本不参与动画的设计,全靠手写。
这个区分很重要,因为它决定了你遇到问题时该往哪儿查。动画没动,不要怀疑 IDE 配置,要去查属性名是否匹配、对象是否还活着、值有没有被别的地方覆盖。我做项目时见过同事因为动画不动,反复重装 Qt、换版本、改构建套件,折腾一整天,最后发现是Q_PROPERTY少了WRITE访问器。方向错了,努力就是白费。
还有一个容易混淆的点:Qt 版本差异。Qt 5.15 和 Qt 6.x 在动画框架上接口基本一致,QPropertyAnimation的用法没变,但 Qt 6 对属性系统的绑定(binding)能力做了增强,QProperty和QBinding相关接口是新的。如果你的项目还在 Qt 5.9 或 5.15 上,本文的写法可以直接用;如果是 Qt 6,绝大部分同样适用,只有涉及绑定表达式的地方需要额外注意。写之前先确认自己的 Qt 版本,Qt Creator 的"帮助 - 关于"里能直接看到。
1.2 QTimer 手搓动画的三个短板
很多人的第一版动画是用QTimer写的:开一个 16ms 的定时器,每次触发就把控件的move()挪几个像素,挪到目标位置就stop()。代码很短,二十行搞定,看起来挺好用。但用久了会发现三个问题。
第一是时间基准不稳。QTimer的精度受事件循环影响,主线程一忙,帧间隔就从 16ms 变成 30ms、50ms,动画看起来一顿一顿的。你没法保证"总时长一定是 300ms",只能保证"大概挪了 300ms 那么多帧",实际耗时可能 400ms 也可能 600ms。而QPropertyAnimation内部基于统一的动画时间源做插值,即使某几帧丢了,它也会根据已经流逝的真实时间直接跳到对应进度,总时长始终是准的。
第二是插值要自己写。想让动画有加速度感,得自己实现缓动公式,比如t*t做二次加速,或者套一个贝塞尔函数。想换个手感就得改公式、重新编译、再跑一遍看效果。而 Qt 内置了四十多种QEasingCurve,改一行setEasingCurve()就能对比效果,试错成本极低。
第三是类型支持差。QTimer方案里你手动算的是数字,要动画化QRect、QPointF、QColor就得自己拆分量、再拼回去。QPropertyAnimation内置了这些类型的插值器,直接给起止值就行。这三点加起来,手搓方案只适合那种"随便动一下就行"的场景,正经项目还是用框架。
1.3 QPropertyAnimation 的三要素与内部驱动方式
一个单动画要跑起来,本质只需要三样东西:动谁(target)、动它的哪个属性(propertyName)、动多久(duration)。起止值不写也行,Qt 会自动取属性当前值作为起点,但终点必须给,否则动画不知道往哪儿走。
QPropertyAnimation *anim = new QPropertyAnimation(targetObject, "propertyName", parent); anim->setDuration(300); anim->setEndValue(endValue); anim->start();内部机制是这样的:QPropertyAnimation是QVariantAnimation的子类,QVariantAnimation又是QAbstractAnimation的子类。QAbstractAnimation负责时间推进,它挂在 Qt 的全局动画驱动上,由事件循环定时唤醒(默认约 60fps 的节奏,实际取决于平台和QAbstractAnimation的更新间隔)。每次唤醒时,QAbstractAnimation计算出当前的进度值(0.0 到 1.0 之间的一个qreal),交给QEasingCurve做一次变换,把线性进度变成缓动进度,再由QVariantAnimation用对应类型的插值器算出中间值,最后QPropertyAnimation通过元对象系统把这个值写回目标属性。写回走的是QMetaProperty::write(),所以属性必须是可写的、且类型能被QVariant承载。
理解这条链路的价值在于:动画不出效果时,你可以按"时间有没有推进 → 插值有没有产出 → 属性有没有写成功"三段分别排查,而不是瞎猜。绝大多数问题在第三段。
2. 单个 QPropertyAnimation 的最小可用骨架
2.1 属性名传错时的第一现场排查
QPropertyAnimation的第二个参数是QByteArray,字符串匹配,编译期不检查。写错一个字母,编译器一声不吭,运行起来动画就是不动。这是新手最常见的翻车点。
我一般这样处理:能在代码里直接传字面量就传,同时打开 Qt Creator 的应用程序输出窗口,Qt 在属性不存在时通常会打印类似QPropertyAnimation: you're trying to animate a non-existing property xxx of your QObject的警告。养成看输出窗口的习惯,能省掉大量抓耳挠腮的时间。
需要确认某个类有哪些属性时,最靠谱的方法是查它的头文件里Q_PROPERTY声明。比如QWidget的geometry、pos、size、windowOpacity、minimumSize、maximumSize都是可动画的;QGraphicsOpacityEffect有opacity;QPushButton自己没有额外属性,但它继承自QWidget,所以geometry之类照样能用。查不到的属性一律视为不可动画,别抱侥幸心理。
2.2 起止值该不该手写
setStartValue()可写可不写,我的经验是分场景决定。如果动画是"从当前状态动到某个固定终点",比如按钮点击后滑到屏幕边缘,那起点不写,让 Qt 自动取当前值,效果最自然。如果你手写了起点,而这个起点跟控件实际位置不一致,动画开头会有一个瞬间跳变——从实际位置"闪"到你设的起点,观感非常差。
反过来,如果是"每次都要回到固定起点再动"的循环动画,比如呼吸灯效果,那起点必须显式指定,否则第二次循环时当前值已经变了,动画会越跑越偏。
提示:起点不写的时候,Qt 取的是
start()调用那一刻的属性值。所以不要在start()之后再改这个属性,否则动画的第一帧会被你改乱。
2.3 一个可以直接粘贴运行的按钮滑入示例
下面这段代码放在一个QWidget子类的构造函数里就能跑,假设界面上有个叫ui->btnSlide的按钮。
#include <QPropertyAnimation> #include <QEasingCurve> #include <QRect> void MainWindow::slideInButton() { // 按钮宽度先拿到,避免动画过程中宽度被改动导致抖动 const int w = ui->btnSlide->width(); const int h = ui->btnSlide->height(); QPropertyAnimation *anim = new QPropertyAnimation(ui->btnSlide, "geometry", this); anim->setDuration(320); // 起点在可视区域左侧外面,视觉上像是"滑进来" anim->setStartValue(QRect(-w, 40, w, h)); anim->setEndValue(QRect(20, 40, w, h)); anim->setEasingCurve(QEasingCurve::OutCubic); anim->start(QAbstractAnimation::DeleteWhenStopped); }这里有两个细节值得说。第一,geometry动画同时改位置和尺寸,比单用pos更稳,因为某些平台下单独动pos和布局系统交互时会有细微偏差。第二,宽高提前用const存下来,是因为按钮的文字可能因为样式变化导致sizeHint变动,如果动画过程中宽度被重算,视觉上会看到按钮在"变宽的同时移动",很难看。
第三,DeleteWhenStopped这个参数是重点,下一节专门讲。
3. QEasingCurve 决定手感:同样 320ms 为什么观感差这么多
3.1 缓动曲线在插值中的位置
前面提到,QAbstractAnimation算出来的是 0.0 到 1.0 的线性进度。假设时长 300ms,在第 150ms 时线性进度就是 0.5。如果直接把 0.5 拿去插值,动画就是匀速的——机械、生硬,像是老式幻灯片切换。
QEasingCurve的作用就是把这个 0.5 变换成另一个值。比如OutCubic在 0.5 处大约返回 0.875,意思是时间过了一半,但位置已经走了 87.5%,剩下的时间用来慢慢收尾。这就是"开头快、结尾缓"的来源。反过来InCubic在 0.5 处返回 0.125,是"起步慢、后段猛冲"。
这个理解很关键:缓动曲线只影响进度映射,不影响总时长。你改了曲线,动画跑完还是 320ms,只是中间每一帧的位置变了。所以调整手感的时候不要老是去改setDuration(),先换曲线试试,往往换个曲线就把问题解决了。
3.2 常用曲线与使用场景对照
内置曲线有四十多种,实际项目里高频使用的就那么几个。我按实际用下来的感受整理了一下。
| 枚举值 | 手感特征 | 我一般用在哪 |
|---|---|---|
| Linear | 严格匀速 | 无限循环的旋转、进度条增长 |
| InOutQuad | 两端都轻缓,幅度小 | 小范围位移,比如图标轻微跳动 |
| OutCubic | 起步利落、收尾从容 | 侧边栏滑出、抽屉展开,最通用 |
| InOutCubic | 两端慢中间快 | 全屏页面的位置切换 |
| OutBack | 末端冲过头再回弹 | 弹窗、气泡提示出现 |
| OutElastic | 明显的弹性振荡 | 需要强视觉提示的强调动效 |
| OutBounce | 像小球落地反复弹跳 | 游戏化场景、成就提示 |
用的时候有个经验:动效越频繁,曲线要越克制。一个用户每天要点几十次的按钮,配OutElastic会让人烦躁;而一年弹一次的成就提示,用OutBounce就很合适。OutCubic之所以成为默认选择,就是因为它够自然、不抢戏。
3.3 贝塞尔自定义与实际参数微调
内置曲线不够用时,可以自己画。Qt 提供了QEasingCurve::BezierSpline类型配合addCubicBezierSegment(),参数含义和 CSS 里的cubic-bezier()完全一致——两个控制点加一个终点,起点固定在 (0,0)。
QEasingCurve curve(QEasingCurve::BezierSpline); curve.addCubicBezierSegment( QPointF(0.25, 0.1), // 控制点1 QPointF(0.25, 1.0), // 控制点2 QPointF(1.0, 1.0) // 终点,通常是 (1,1) ); anim->setEasingCurve(curve);参数怎么调?控制点1 的 x 越小,起步越慢;控制点2 的 x 越大,收尾越拖。上面这组参数是经典的cubic-bezier(0.25, 0.1, 0.25, 1),特点是起步慢、中段快、末段缓,用在卡片翻页上很舒服。
另外几个内置曲线还有可调的附加参数:OutElastic和OutBack有setOvershoot(),控制过冲的幅度,默认 1.70158,调到 3 以上会非常夸张;OutElastic还有setAmplitude()和setPeriod(),分别控制振幅和振荡周期。建议先把amplitudeset调小到 0.5 左右再试,默认值对界面元素来说偏猛。
注意:
setOvershoot()这类参数只对特定曲线类型生效,其他类型上调用是空操作,别以为设了就有变化。
4. 动画对象到底归谁管:生命周期与内存归属
4.1 start() 的 DeleteWhenStopped 参数
start()有一个默认参数QAbstractAnimation::KeepWhenStopped,意思是动画停止后对象保留,由你负责释放。另一个选项是DeleteWhenStopped,动画跑完自动删除自己。
这两个选项选错了,就是内存泄漏和悬空指针两条路。最要命的是这个组合:对象用DeleteWhenStopped创建,但你把它存在成员变量里,下次点击时直接调m_anim->start()。第一次跑完,对象已经被删了,m_anim变成悬空指针,第二次点击直接崩溃。这种崩溃在调试器里看到的是随机地址访问错误,很难联想到是动画引起的。
我的规则很简单:**一次性动画用DeleteWhenStopped,且绝不保存指针;需要反复使用的动画用KeepWhenStopped,存成员变量,只创建一次。**不要两种混着来。
4.2 成员变量 vs 局部变量:两种写法的不同下场
局部变量写法最省事,用完就没了:
void MainWindow::flashSomething() { auto *anim = new QPropertyAnimation(ui->labelTip, "windowOpacity", this); anim->setDuration(200); anim->setStartValue(1.0); anim->setEndValue(0.0); anim->start(QAbstractAnimation::DeleteWhenStopped); }注意构造函数第三个参数传了this作为父对象。这层父子关系是兜底:即使动画因为某些原因没走到停止状态(比如程序中途退出),父对象析构时也会一并清理,不会泄漏。很多人写new QPropertyAnimation(...)时把 parent 传nullptr,然后忘了释放,跑一次泄漏一个,长时间运行的小工具内存会慢慢涨上去。
成员变量写法适合复用:
// 头文件 QPropertyAnimation *m_panelAnim = nullptr; // 构造函数里初始化一次 m_panelAnim = new QPropertyAnimation(ui->panel, "geometry", this); m_panelAnim->setDuration(280); m_panelAnim->setEasingCurve(QEasingCurve::OutCubic); // 要用的时候 void MainWindow::togglePanel(bool show) { m_panelAnim->stop(); // 关键:先停掉,避免中途重复启动 m_panelAnim->setStartValue(ui->panel->geometry()); m_panelAnim->setEndValue(show ? shownRect : hiddenRect); m_panelAnim->start(); }这里m_panelAnim->stop()那一行看着多余,其实是保命操作,原因在 6.1 节展开。
4.3 动画结束后的收尾动作
QPropertyAnimation有finished()信号,动画正常跑完会发出来。常见的收尾动作有两类:一类是"跑完之后改控件状态",比如滑出动画结束后把面板hide();另一类是"跑完之后释放资源",比如删掉临时创建的图形效果。
但有个坑要注意:stop()不会触发finished()。只有动画自然跑完、或者被setCurrentTime()推到了终点,才会发finished()。所以如果你的收尾逻辑放在finished()里,而某个分支走了stop(),那段逻辑就不会执行,很容易出现"状态没复位"的诡异现象。凡是stop()的分支,都要手动补一遍收尾代码,或者把收尾逻辑抽成一个函数,两边都调。
另外,finished()触发的时刻是动画时间走完,此时属性值已经写到了终点值。这一点在需要"接着做下一个动作"时很重要,不用担心读到的是中间值。
5. 属性动不了?Q_PROPERTY 与可动画属性的硬性约束
5.1 QMetaProperty 的 writable 检查
QPropertyAnimation要写属性,走的是元对象系统,要求属性满足两个条件:在类声明里用Q_PROPERTY注册过,且带有WRITE访问器(或者MEMBER,Qt 5.6+ 支持)。
只写READ不写WRITE的属性,QPropertyAnimation是动不了的。这种情况下 Qt 通常不报警告,动画就是静默失效。我遇到过一位同事,给一个只读的Q_PROPERTY加动画,调了两小时,最后在setStartValue()的地方反复怀疑类型转换问题,其实压根就是属性不可写。
排查方法很直接:用targetObject->metaObject()->indexOfProperty("xxx")拿到索引,再用property(index).isWritable()判断。写个三行的调试代码,一眼就能确认。
const QMetaObject *mo = ui->myWidget->metaObject(); int idx = mo->indexOfProperty("progress"); if (idx < 0) { qDebug() << "属性根本不存在"; } else if (!mo->property(idx).isWritable()) { qDebug() << "属性存在但不可写"; }5.2 给自定义控件加一个可动画属性
自定义控件想被动画化,标准做法是加一个Q_PROPERTY。假如你写了个环形进度控件,想让它的progress值平滑增长:
class RingGauge : public QWidget { Q_OBJECT Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged) public: qreal progress() const { return m_progress; } void setProgress(qreal v) { if (qFuzzyCompare(m_progress, v)) return; m_progress = v; update(); // 触发重绘 emit progressChanged(); } signals: void progressChanged(); private: qreal m_progress = 0.0; };关键点有三个。第一,setProgress()里要做相等判断,否则动画每帧都会发update(),即使值没变,重绘开销白白浪费。第二,update()是必须的,因为QWidget不会因为你改了内部变量就自动重绘。第三,NOTIFY信号在纯动画场景下不是必须的,但加上更好,方便别处绑定或联动。
动画化的时候属性名传"progress",类型是qreal,QVariantAnimation内置了double的插值器,直接能用。
5.3 非 QObject 场景:QGraphicsItem 与 QVariantAnimation 兜底
QGraphicsItem不是QObject,所以不能直接用QPropertyAnimation动画化。绕过的方法有两条:一是改用QGraphicsObject(继承QObject和QGraphicsItem),代价是每个图元多了一点开销;二是用QVariantAnimation自己接信号,手动写属性。
auto *anim = new QVariantAnimation(this); anim->setStartValue(0.0); anim->setEndValue(1.0); anim->setDuration(500); anim->setEasingCurve(QEasingCurve::OutQuad); connect(anim, &QVariantAnimation::valueChanged, this, [item](const QVariant &v) { item->setOpacity(v.toReal()); }); anim->start(QAbstractAnimation::DeleteWhenStopped);QVariantAnimation的灵活性更高,还能动画化那些没有Q_PROPERTY的普通变量、甚至联动多个对象。代价是要自己写写入逻辑,缓动和时间控制还是框架帮你做。图元数量多、性能敏感的场景,我会优先选这条路,因为省掉了QGraphicsObject的信号槽开销。
6. 单个动画的六个边界情况
6.1 连点按钮触发的属性竞态
用户手快,动画还没跑完就点了第二次。如果你不做处理,第二次start()会直接接管现有的动画对象(QPropertyAnimation在运行中再调start()会重新开始计时),起点变成当前这个"走到一半"的位置。看起来好像也没问题——从中间位置继续滑到终点,实际上多数时候一眼就能看出不自然,因为缓动曲线是从头跑的,前段的速度感会突然变。
正确做法是先把起止值锁定:要么stop()后重新setStartValue(当前值),要么判断状态——如果动画已经在跑,就直接忽略这次点击。做侧边栏抽屉的时候我一般选后者:动画期间按钮置灰或者加一个bool m_animating标志位,finished()里复位。宁可让用户多点一次,也不要让抽屉滑到一半被强行拽回来。
如果是那种"来回切换"的开关,比如面板展开/收起,就必须允许中断。此时推荐的做法是:stop()之后取当前 geometry 作为新起点,终点按目标状态选,重新start()。这样视觉上是从中间位置平滑折返,是可接受的。
6.2 布局管理器把动画"拉回原位"
这个坑很常见:你给一个在QVBoxLayout/QHBoxLayout里的控件做geometry动画,动画期间框架确实在改位置,但某个时刻布局系统重新计算了一遍,把控件拍回原位,于是你看到控件动了一下又弹回去。
根因是布局管理器拥有子控件的几何信息,只要布局被触发(比如父窗口 resize、某个子控件sizeHint变化、updateGeometry()被调用),它就会重新设置所有子控件的几何。你在动画里写的值会被覆盖。
解决办法有三种,按推荐度排序。第一,动画化不被布局管理的控件:把要动的控件放进一个自由摆放的容器里,这个容器本身在布局中,但容器内的子控件不受布局约束。第二,动画化布局相关的属性,比如maximumWidth、minimumHeight,让布局自己根据这些值重排,效果一样平滑,还不打架。第三,临时把控件从布局里摘出来,动画结束后再放回去——可行但很别扭,非必要不用。
第二种做法在折叠面板上特别好用:动画maximumHeight从 0 到sizeHint().height(),收起时反过来,布局自动跟着变,连滚动条都会自然处理。
6.3 高 DPI 下的取整抖动与坐标偏移
在缩放比例 125%、150% 的屏幕上做动画,偶尔会看到控件边缘有半像素的抖动。原因是QWidget::geometry用的是整数QRect,而QPropertyAnimation插值出来的QRect是浮点算完取整的,逐帧取整的误差会让边界来回跳一像素。
处理方式:位移类动画尽量用无缩放的、对数不敏感的方式表达,比如用move配合QPointF计算再取整,或者干脆接受一像素的抖动——大多数场景下肉眼看不出来。真正影响观感的是带圆角或描边的控件,那时候一像素抖动会被放大。如果确实需要极致平滑,可以走QGraphicsView体系,图元支持qreal坐标,配合setRenderHint(QPainter::Antialiasing),效果会干净很多。
另外,跨屏拖动窗口时 DPI 变化,动画中的坐标可能在切换瞬间产生跳变,这属于平台特性,建议在动画期间锁定窗口所在屏幕,或者干脆用windowOpacity这种不受坐标影响的属性做过渡。
6.4 窗口最小化、控件隐藏时的空转
控件hide()之后,QPropertyAnimation依然在跑,只是在改一个不可见对象的属性,纯浪费 CPU。更麻烦的是父窗口最小化时,某些平台下动画更新会被暂停,恢复的时候可能出现时间跳跃,动画瞬间跳到终点。
我的处理习惯是:动画开始前检查isVisible(),不可见就直接把属性设成终点值,跳过动画。窗口最小化的场景,监听QWindow::visibilityChanged,不可见时pause(),恢复时resume()。这样既不浪费资源,也不会出现恢复后"闪现"。
还有一个无障碍相关的点值得提:系统里如果用户开启了"减少动态效果",动画应该被弱化或跳过。Qt 这边没有统一的无障碍动画开关,需要在应用层自己留个配置项。这个功能做起来很简单,但能体现专业度。
7. QML 里写同一个动画的对照写法
7.1 PropertyAnimation 与 Behavior 的区别
同样的需求换到 QML,写法差别不小。QML 里最直接的写法是PropertyAnimation on x,它等价于 C++ 里的QPropertyAnimation动画化x属性:
Rectangle { id: box width: 80; height: 80 color: "#3a7afe" PropertyAnimation on x { from: 0 to: 240 duration: 320 easing.type: Easing.OutCubic running: true } }on x这种内联声明会常驻监听,running控制开关。而Behavior on x是另一种思路——它不是主动播放动画,而是拦截x的所有赋值操作,自动用动画过渡:
Rectangle { id: box x: 0 Behavior on x { NumberAnimation { duration: 320; easing.type: Easing.OutCubic } } MouseArea { anchors.fill: parent onClicked: box.x = box.x > 0 ? 0 : 240 } }两者的选择很清楚:动画次数固定、需要精确控制播放时机,用PropertyAnimation;属性随时可能被各处修改、希望每次变化都有过渡,用Behavior。Behavior写起来更简洁,但如果某个赋值不想带动画,需要临时Behavior.enabled = false,这一点容易被忽略。
7.2 在 Qt Creator 里调试 QML 动画的方式
QML 动画的调试体验比 Widgets 好一些。Qt Creator 里打开 QML Profiler,可以看到每一帧的耗时、绑定的求值次数、动画的持续区间。定位"动画掉帧"的问题时,先看是不是某帧超过 16ms,再看那一帧里是不是有大量绑定在重新求值。
另一个实用技巧是用Animation的onStopped和onRunningChanged打日志,确认动画有没有真的播完。我遇到过因为running绑定了一个始终为false的表达式导致动画完全不播的情况,日志一看就明白了。
Widgets 那边没有 QML Profiler,但可以用 Qt Creator 自带的性能分析器配合QElapsedTimer在动画帧里打时间戳。虽然原始,但排查"哪一帧卡了"够用了。更简单的办法是把duration临时调到 3000ms,人眼慢慢看,很多问题放慢之后一目了然——这个土办法我用得比分析器还多。
动画这套东西真正的门槛不在 API,而在手感。参数、曲线、时长这些数值,文档里给不出标准答案,只能自己反复试。我的建议是建一个专门的小工程,把常用的几种过渡都写一遍,调到自己满意,之后做项目直接抄过来。省下来的时间,比背多少个 API 都值。