动画引擎架构——统一调度的生命线管理¶
在 Layer 2 里,我们聊了主题系统的各个组件。但有了颜色和字体还不够,Material Design 3 的灵魂在于动效——那些流畅的过渡、自然的弹性运动。
这篇文章聊聊动画引擎的架构设计。
为什么需要统一的动画引擎?¶
Qt 已经提供了 QPropertyAnimation、QVariantAnimation 等动画类,为什么还要自己实现?
有几个原因:
- Material Design 3 的特殊需求:MD3 定义了一套标准的动画时长和缓动曲线(shortEnter、mediumEnter 等),Qt 的默认配置不匹配。
- 生命周期管理问题:如果每个控件都自己创建动画,很容易出现生命周期混乱——控件销毁了但动画还在运行,或者动画结束了但控件已经不存在了。
- 性能优化:统一的动画引擎可以全局控制动画开关(比如用户禁用动画、嵌入式环境精简),避免零散的动画难以管理。
- 弹簧动画支持:Qt 没有提供弹簧动画的原生支持,而 MD3 大量使用弹簧效果。
ICFAbstractAnimation:动画基类¶
所有动画都继承自 ICFAbstractAnimation,它定义了动画的核心接口:
class ICFAbstractAnimation : public QObject {
public:
enum class State { Idle, Running, Paused, Finished };
enum class Direction { Forward, Backward };
virtual void start(Direction dir = Direction::Forward) = 0;
virtual void pause() = 0;
virtual void stop() = 0;
virtual void reverse() = 0;
virtual bool tick(int dt) = 0;
virtual cf::WeakPtr<ICFAbstractAnimation> GetWeakPtr() = 0;
signals:
void started();
void paused();
void stopped();
void finished();
void progressChanged(float progress);
protected:
float m_progress = 0.0f;
State m_state = State::Idle;
};
这里有个关键设计:GetWeakPtr() 返回的是弱引用。原因我们后面会讲,但核心思想是:动画由工厂拥有所有权,用户只持有弱引用。
动画状态机¶
动画有四种状态:
- Idle:动画尚未开始或已停止
- Running:动画正在运行
- Paused:动画已暂停
- Finished:动画已完成
状态转换遵循以下规则:
Idle → Running (start)
Running → Paused (pause)
Paused → Running (start)
Running/Paused → Idle (stop)
Running → Finished (自然结束)
Direction 方向控制¶
动画支持两种播放方向:
- Forward:从
from值到to值 - Backward:从
to值到from值
reverse() 方法会停止当前动画,然后以相反方向重新播放。这对于"鼠标进入淡入、鼠标离开淡出"的场景非常实用。
tick() 虚函数¶
tick(int dt) 是动画的核心更新方法,每帧调用一次。dt 是距离上一帧的时间间隔(毫秒)。
每个具体的动画类需要实现自己的 tick() 逻辑:
- TimingAnimation:根据
m_elapsed和缓动曲线计算进度 - SpringAnimation:使用弹簧物理公式计算位置和速度
tick() 返回 true 表示动画继续,false 表示动画已完成。
WeakPtr 所有权模型¶
这是一个关键设计。动画的所有权结构是:
CFMaterialAnimationFactory (owner)
└── unordered_map<string, unique_ptr<ICFAbstractAnimation>> animations_
Controls
└── WeakPtr<ICFAbstractAnimation>
工厂拥有动画的所有权(unique_ptr),控件只持有弱引用(WeakPtr)。这样设计的好处是:
- 生命周期安全:控件销毁时,弱引用自动失效,不会访问野指针
- 统一管理:工厂销毁时,所有动画自动销毁
- 共享访问:多个控件可以共享同一个动画实例
获取动画的典型流程:
auto anim = factory->getAnimation("md.animation.fadeIn");
if (anim) { // 检查 WeakPtr 是否有效
connect(anim.get(), &ICFAbstractAnimation::progressChanged,
this, [this](float progress) {
update();
});
anim->start();
}
progressChanged 信号¶
每次 tick() 更新进度后,动画会发出 progressChanged 信号。控件可以连接这个信号来更新 UI。
注意这里用的是信号机制而不是 QProperty。原因是:
- 性能:信号机制比 QProperty 更轻量
- 灵活性:控件可以选择是否监听进度变化
- 兼容性:不依赖 Qt 6 的新特性
全局动画开关¶
ICFAnimationManagerFactory 提供了全局开关:
禁用时,getAnimation() 会返回无效的 WeakPtr,这样就不会创建新动画。已有的正在运行的动画不受影响,会自然完成。
这个功能对以下场景很有用:
- 性能优化:繁重任务期间禁用动画
- 无障碍:尊重系统的"减少动画"设置
- 用户偏好:提供一个"禁用动画"的选项
目标 FPS 设置¶
工厂可以设置目标 FPS:
这会影响动画的定时器间隔。更高的 FPS 意味着更平滑的动画,但也意味着更多的 CPU 开销。
总结¶
动画引擎的核心设计是"统一调度 + WeakPtr 所有权"。工厂拥有动画,用户持有弱引用,这样既保证了生命周期安全,又提供了灵活的访问方式。
但抽象基类只是定义接口,具体的动画还需要实现。Material Design 3 用两种主要的动画范式:基于时间的动画和基于弹簧的动画。
接下来,我们深入这两种动画的具体实现。
相关文档