跳转至

动画引擎架构——统一调度的生命线管理

在 Layer 2 里,我们聊了主题系统的各个组件。但有了颜色和字体还不够,Material Design 3 的灵魂在于动效——那些流畅的过渡、自然的弹性运动。

这篇文章聊聊动画引擎的架构设计。

为什么需要统一的动画引擎?

Qt 已经提供了 QPropertyAnimation、QVariantAnimation 等动画类,为什么还要自己实现?

有几个原因:

  1. Material Design 3 的特殊需求:MD3 定义了一套标准的动画时长和缓动曲线(shortEnter、mediumEnter 等),Qt 的默认配置不匹配。
  2. 生命周期管理问题:如果每个控件都自己创建动画,很容易出现生命周期混乱——控件销毁了但动画还在运行,或者动画结束了但控件已经不存在了。
  3. 性能优化:统一的动画引擎可以全局控制动画开关(比如用户禁用动画、嵌入式环境精简),避免零散的动画难以管理。
  4. 弹簧动画支持: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)。这样设计的好处是:

  1. 生命周期安全:控件销毁时,弱引用自动失效,不会访问野指针
  2. 统一管理:工厂销毁时,所有动画自动销毁
  3. 共享访问:多个控件可以共享同一个动画实例

获取动画的典型流程:

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。原因是:

  1. 性能:信号机制比 QProperty 更轻量
  2. 灵活性:控件可以选择是否监听进度变化
  3. 兼容性:不依赖 Qt 6 的新特性

全局动画开关

ICFAnimationManagerFactory 提供了全局开关:

factory->setEnabledAll(false);  // 禁用所有动画
factory->setEnabledAll(true);   // 启用所有动画

禁用时,getAnimation() 会返回无效的 WeakPtr,这样就不会创建新动画。已有的正在运行的动画不受影响,会自然完成。

这个功能对以下场景很有用:

  • 性能优化:繁重任务期间禁用动画
  • 无障碍:尊重系统的"减少动画"设置
  • 用户偏好:提供一个"禁用动画"的选项

目标 FPS 设置

工厂可以设置目标 FPS:

factory->setTargetFps(60.0f);  // 60 FPS

这会影响动画的定时器间隔。更高的 FPS 意味着更平滑的动画,但也意味着更多的 CPU 开销。

总结

动画引擎的核心设计是"统一调度 + WeakPtr 所有权"。工厂拥有动画,用户持有弱引用,这样既保证了生命周期安全,又提供了灵活的访问方式。

但抽象基类只是定义接口,具体的动画还需要实现。Material Design 3 用两种主要的动画范式:基于时间的动画和基于弹簧的动画。

接下来,我们深入这两种动画的具体实现。


相关文档