工厂与策略——动画创建的灵活组合模式¶
在前面的文章里,我们讲了动画的抽象基类和两种具体实现。但控件怎么获取这些动画?直接 new 一个吗?
不,那样会让控件和动画类型耦合,难以扩展和测试。我们用了工厂模式 + 策略模式的组合来解决这个问题。
CFMaterialAnimationFactory 的职责¶
CFMaterialAnimationFactory 是 Material Design 3 动画的核心工厂,它负责:
- Token 解析:将 "md.animation.fadeIn" 这样的 token 转换为动画描述
- 动画创建:根据描述创建具体的动画实例
- 生命周期管理:拥有创建的动画,提供 WeakPtr 访问
- 全局开关:支持启用/禁用所有动画
- 策略应用:允许控件类型定制动画行为
class CFMaterialAnimationFactory : public ICFAnimationManagerFactory {
public:
explicit CFMaterialAnimationFactory(const ICFTheme& theme,
std::unique_ptr<AnimationStrategy> strategy = nullptr,
QObject* parent = nullptr);
cf::WeakPtr<ICFAbstractAnimation> getAnimation(const char* animationToken) override;
void setStrategy(std::unique_ptr<AnimationStrategy> strategy);
void setEnabledAll(bool enabled) override;
private:
const ICFTheme& theme_;
std::unique_ptr<AnimationStrategy> strategy_;
bool globalEnabled_ = true;
std::unordered_map<std::string, std::unique_ptr<ICFAbstractAnimation>> animations_;
};
Token 到 AnimationDescriptor 的映射¶
Material Design 3 定义了标准的动画 Token,比如:
| Token | 类型 | MotionSpec |
|---|---|---|
| md.animation.fadeIn | fade | shortEnter |
| md.animation.fadeOut | fade | shortExit |
| md.animation.slideUp | slide | mediumEnter |
| md.animation.slideDown | slide | mediumExit |
| md.animation.scaleUp | scale | shortEnter |
| md.animation.scaleDown | scale | shortExit |
工厂内部维护了一个映射表,将 Token 转换为 AnimationDescriptor:
struct AnimationDescriptor {
QString type; // "fade", "slide", "scale"
QString motionToken; // "md.motion.shortEnter" 等
QString targetProperty; // "opacity", "position", "scale"
float fromValue;
float toValue;
};
getAnimation() 的完整流程¶
当控件调用 getAnimation("md.animation.fadeIn") 时,工厂会执行以下步骤:
- 检查
globalEnabled_,如果为 false 返回无效 WeakPtr - 检查是否已有同名动画,如果有返回缓存的动画
- 解析 Token 到 AnimationDescriptor
- 应用策略(如果设置了)调整描述符
- 根据类型创建具体的动画实例
- 存储动画(获得所有权)
- 返回 WeakPtr 给调用者
cf::WeakPtr<ICFAbstractAnimation> CFMaterialAnimationFactory::getAnimation(const char* token) {
// 1. 全局开关检查
if (!globalEnabled_) {
return cf::WeakPtr<ICFAbstractAnimation>();
}
// 2. 查找缓存
auto it = animations_.find(token);
if (it != animations_.end()) {
return it->second->GetWeakPtr();
}
// 3. 解析 Token
AnimationDescriptor desc = resolveToken(token);
if (desc.type.isEmpty()) {
return cf::WeakPtr<ICFAbstractAnimation>();
}
// 4-5. 应用策略并创建动画
desc = applyStrategy(desc, nullptr);
auto anim = createAnimation(desc, nullptr);
if (!anim) {
return cf::WeakPtr<ICFAbstractAnimation>();
}
// 6. 存储动画
animations_[token] = std::move(anim);
// 7. 返回 WeakPtr
return animations_[token]->GetWeakPtr();
}
动画实例的创建¶
工厂根据 AnimationDescriptor::type 创建不同类型的动画:
std::unique_ptr<ICFAbstractAnimation> CFMaterialAnimationFactory::createFadeAnimation(
const AnimationDescriptor& desc, QWidget* widget) {
auto motionSpec = const_cast<IMotionSpec*>(&theme_.motion_spec());
auto anim = std::make_unique<CFMaterialFadeAnimation>(motionSpec, this);
anim->setRange(desc.fromValue, desc.toValue);
anim->setMotionToken(desc.motionToken.toStdString());
return anim;
}
std::unique_ptr<ICFAbstractAnimation> CFMaterialAnimationFactory::createSlideAnimation(...) {
// 类似的逻辑,但创建的是 CFMaterialSlideAnimation
}
std::unique_ptr<ICFAbstractAnimation> CFMaterialAnimationFactory::createScaleAnimation(...) {
// 创建 CFMaterialScaleAnimation
}
AnimationStrategy 策略模式¶
有时候,不同类型的控件需要不同的动画行为。比如按钮可能需要更快的动画,对话框可能需要更慢的动画。
策略模式允许在不修改工厂代码的情况下定制动画行为:
class AnimationStrategy {
public:
virtual ~AnimationStrategy() = default;
virtual AnimationDescriptor adjust(const AnimationDescriptor& desc,
QWidget* widget) {
return desc; // 默认不做修改
}
virtual bool shouldEnable(QWidget* widget) {
return true; // 默认启用动画
}
};
控件类型可以实现自己的策略:
class ButtonAnimationStrategy : public AnimationStrategy {
public:
AnimationDescriptor adjust(const AnimationDescriptor& desc, QWidget* widget) override {
// 按钮的动画更快
AnimationDescriptor adjusted = desc;
adjusted.fromValue = desc.fromValue * 0.8f;
adjusted.toValue = desc.toValue * 1.2f;
return adjusted;
}
};
class DialogAnimationStrategy : public AnimationStrategy {
public:
AnimationDescriptor adjust(const AnimationDescriptor& desc, QWidget* widget) override {
// 对话框的动画更慢、更平滑
AnimationDescriptor adjusted = desc;
adjusted.motionToken = "md.motion.longEnter";
return adjusted;
}
};
使用策略:
// 创建工厂时设置策略
auto buttonStrategy = std::make_unique<ButtonAnimationStrategy>();
auto factory = std::make_unique<CFMaterialAnimationFactory>(theme, std::move(buttonStrategy));
// 控件获取动画时会自动应用策略
auto anim = factory->getAnimation("md.animation.fadeIn");
策略的应用时机¶
策略在动画创建之前应用,这样可以在创建前调整参数。但策略只影响新创建的动画,已经创建的动画不受影响。
如果需要动态更换策略,可以再次调用 setStrategy(),之后创建的动画会使用新策略。
全局动画开关¶
工厂提供了全局开关功能:
禁用时,getAnimation() 会返回无效的 WeakPtr。已有的正在运行的动画不受影响,会自然完成。
这个功能对以下场景很有用:
- 性能优化:繁重任务期间禁用动画
- 无障碍:尊重系统的"减少动画"设置
- 用户偏好:提供一个"禁用动画"的设置选项
- 嵌入式环境:资源受限时禁用所有动画
单个动画的启用/禁用¶
工厂也支持单个动画的启用/禁用:
这样只有特定的动画被禁用,其他动画正常运行。
动画缓存¶
工厂会缓存已创建的动画。当多次调用 getAnimation("md.animation.fadeIn") 时,会返回同一个动画实例的 WeakPtr。
这意味着如果你需要多个独立的动画实例,需要用不同的 Token 名称或者使用 createAnimation() 直接创建。
总结¶
工厂模式 + 策略模式的组合让动画系统既统一又灵活。工厂负责创建和管理动画,策略负责定制行为。控件只需要通过 Token 获取动画,不需要关心具体的实现细节。
到这里,Layer 3(Animation Engine Layer)的内容就基本覆盖了。我们有了统一的动画调度系统,有了两种动画类型,有了工厂和策略模式。
但动画只是"效果",还需要有东西来触发这些动画——比如鼠标悬停、点击、焦点变化等交互行为。
接下来,我们进入 Layer 4:Material Behavior Layer,看看如何将 Qt 事件映射到 Material 视觉状态。
相关文档