IMotionSpec - 动画规格接口¶
IMotionSpec 定义了按名称查询动画参数的抽象接口,是 Material Design 3 运动系统的 C++ 映射层。选择把持续时间、缓动类型和延迟分别查询而不是打包成一个结构体,是为了灵活性——有些场景只需要持续时间,有些场景需要完整的三参数组合。
基本用法¶
IMotionSpec 提供了三个查询方法,分别对应动画的三个核心参数:
#include "ui/core/motion_spec.h"
IMotionSpec& motion = theme.motion_spec();
// 查询动画持续时间(毫秒)
int duration = motion.queryDuration("md.motion.shortEnter"); // 200ms
int standard = motion.queryDuration("md.motion.standard"); // 400ms
// 查询缓动类型(枚举值)
int easing = motion.queryEasing("md.motion.standard"); // 线性/缓入缓出
// 查询动画延迟(毫秒)
int delay = motion.queryDelay("md.motion.shortEnter"); // 通常为 0
这三个参数组合起来可以构造完整的动画配置:
// 使用 QPropertyAnimation
auto* animation = new QPropertyAnimation(button, "geometry");
animation->setDuration(motion.queryDuration("md.motion.standard"));
animation->setEasingCurve(static_cast<QEasingCurve::Type>(
motion.queryEasing("md.motion.standard")
));
animation->setStartValue(startRect);
animation->setEndValue(endRect);
animation->start();
Token 命名约定¶
虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定:
// Material Design 3 运动规范
"md.motion.standard" // 标准运动(400ms,缓入缓出)
"md.motion.shortEnter" // 短进入动画(200ms,缓出)
"md.motion.mediumEnter" // 中等进入动画(300ms,缓出)
"md.motion.longEnter" // 长进入动画(500ms,缓出)
"md.motion.shortExit" // 短退出动画(150ms,缓入)
"md.motion.mediumExit" // 中等退出动画(250ms,缓入)
"md.motion.longExit" // 长退出动画(350ms,缓入)
"md.motion.shortDuration" // 短持续时间(150ms)
"md.motion.mediumDuration" // 中等持续时间(250ms)
"md.motion.longDuration" // 长持续时间(350ms)
"md.motion.extraLongDuration" // 超长持续时间(500ms+)
缓动类型返回的是整数值,需要映射到具体的缓动曲线枚举。Qt 的 QEasingCurve::Type 是一个常见的映射目标:
// 缓动类型映射示例
// 返回值 -> QEasingCurve::Type
// 0 -> Linear
// 1 -> InQuad
// 2 -> OutQuad
// 3 -> InOutQuad
// 32 -> InCubic
// 33 -> OutCubic
// 34 -> InOutCubic
// ...
具体的映射关系由实现类决定,可以在文档中说明。
使用示例¶
在实际 UI 组件中使用动画规格:
// 淡入动画
void fadeInWidget(QWidget* widget, const char* motionToken) {
auto* effect = new QGraphicsOpacityEffect(widget);
widget->setGraphicsEffect(effect);
auto* animation = new QPropertyAnimation(effect, "opacity");
animation->setDuration(motion.queryDuration(motionToken));
animation->setStartValue(0.0);
animation->setEndValue(1.0);
animation->setEasingCurve(static_cast<QEasingCurve::Type>(
motion.queryEasing(motionToken)
));
animation->start(QAbstractAnimation::DeleteWhenStopped);
}
// 使用
fadeInWidget(myWidget, "md.motion.shortEnter"); // 快速淡入
对于需要延迟的动画序列:
void staggeredShow(QList<QWidget*> widgets) {
auto& motion = getTheme().motion_spec();
int staggerDelay = motion.queryDelay("md.motion.staggerDelay"); // 50ms
for (int i = 0; i < widgets.size(); ++i) {
auto* animation = new QPropertyAnimation(widgets[i], "opacity");
animation->setDuration(motion.queryDuration("md.motion.shortEnter"));
animation->setStartValue(0.0);
animation->setEndValue(1.0);
animation->setStartDelay(i * staggerDelay); // 错开延迟
animation->start();
}
}
缓动曲线映射¶
缓动类型返回的整数值需要映射到具体框架的缓动曲线。这里是一个简单的映射辅助类示例:
class EasingCurveMapper {
public:
static QEasingCurve fromMotionValue(int value) {
switch (value) {
case 0: return QEasingCurve::Linear;
case 1: return QEasingCurve::InQuad;
case 2: return QEasingCurve::OutQuad;
case 3: return QEasingCurve::InOutQuad;
case 32: return QEasingCurve::InCubic;
case 33: return QEasingCurve::OutCubic;
case 34: return QEasingCurve::InOutCubic;
default: return QEasingCurve::InOutCubic; // fallback
}
}
};
// 使用
auto curve = EasingCurveMapper::fromMotionValue(
motion.queryEasing("md.motion.standard")
);
animation->setEasingCurve(curve);
实现要点¶
实现 IMotionSpec 时需要考虑几个细节:
-
缓动类型的表示:接口用
int返回缓动类型,而不是定义自己的枚举。这是为了避免向调用方传播 C++ 类型定义——如果接口要被其他语言调用,整数比枚举更容易处理。实现类可以选择内部用枚举,导出时转换。 -
默认延迟值:大多数动画不需要延迟,所以
queryDelay()的默认返回值应该是 0。只有在"交错动画"这类特殊场景下,才需要非零延迟。 -
无效 token 处理:不要抛异常或返回负值。对于持续时间,返回一个合理的默认值(比如 300ms);对于缓动类型,返回一个常用的缓动曲线;对于延迟,返回 0。
-
线程安全:动画规格查询通常发生在 UI 线程,但如果实现类支持跨线程访问,需要在各方法内部加锁。考虑到这些都是简单的哈希查找,锁的开销可以接受。
设计决策¶
IMotionSpec 把三个参数分开查询,而不是返回一个包含三个字段的结构体。这是为了使用场景的灵活性——很多动画只需要持续时间和缓动,延迟总是 0;而有些场景(比如交错动画)只需要延迟参数。如果打包成结构体,调用方总是要构造或接收一个他们不需要的字段。
另一个设计点是,我们用 int 表示时间而不是 std::chrono::duration。这是因为 Qt 的动画 API 用整数毫秒,用标准库类型反而需要转换。如果将来要支持更高精度的时间单位,可以在实现类内部用 std::chrono,导出时转换成毫秒。