跳转至

CFMaterialScaleAnimation - Material 缩放动画

CFMaterialScaleAnimation 是 Material Design 3 运动系统中的尺寸变换实现,控制 widget 以缩放方式出现或消失。与淡入淡出和滑动相比,缩放动画有更强的"强调"意味——它让元素感觉像是"从某个点生长出来",适合用于需要抓住用户注意力的重要交互。

在 Material Design 3 中的定位

Material Design 将缩放归类为"强调性变换",主要用于:

场景 效果 语义
对话框、菜单弹出 从 0.8~0.9 放大到 1.0 聚焦,吸引用户注意
按钮按下效果 缩小到 0.95 触觉反馈
FAB 展开 从小圆点放大成完整按钮 状态展开
图片查看 缩小/放大 空间导航

缩放动画的核心是"中心锚点"——默认情况下,widget 会以其中心点为基准进行缩放,这符合人类对"生长"的直觉认知。

基本用法

缩放动画的构造比较简单,只需要 motion spec:

#include "ui/components/material/cfmaterial_scale_animation.h"

using namespace cf::ui::components::material;

// 创建一个缩放动画
auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec, this);

// 设置目标 widget
scaleAnim->setTargetWidget(myDialog);

// 开始动画(Forward = 放大,Backward = 缩小)
scaleAnim->start(Direction::Forward);

默认的缩放范围是 0.8 到 1.0(从 80% 放大到 100%),这是 Material 推荐的"弹出"效果参数。

中心锚点

缩放动画最关键的设计决策是锚点位置。我们默认从中心点缩放(scaleFromCenter = true),这样 widget 的中心位置保持不变,四周均匀缩放:

// 默认行为:从中心缩放
auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec);
scaleAnim->setScaleFromCenter(true);  // 这是默认值
scaleAnim->setTargetWidget(widget);
scaleAnim->start();

// widget 会保持中心位置不变,四周同时收缩/扩张

如果需要从左上角或其他位置缩放,可以关闭中心缩放模式:

scaleAnim->setScaleFromCenter(false);
// 此时缩放会以 widget 几何原点(左上角)为锚点

⚠️ 非中心缩放在某些布局下可能会导致 widget 位置发生明显偏移,使用时需要谨慎测试。

几何变换实现

我们的缩放是通过修改 widget 的 setGeometry() 实现的,而不是使用 QTransform。这个选择是经过权衡的:

方案 优点 缺点
setGeometry 布局兼容性好,重绘正确 性能稍低,需要手动计算
QTransform 性能更好,硬件加速 可能导致布局错乱,边距问题

使用 setGeometry 意味着动画过程中 widget 的实际几何属性在变化,布局系统和父容器会正确感知这个变化。如果你在动画期间需要查询 widget 的尺寸,得到的是实时缩放后的值:

scaleAnim->start();
// ... 动画进行中 ...
qDebug() << widget->width();   // 当前缩放后的宽度
qDebug() << widget->height();  // 当前缩放后的高度

时序参数

缩放动画的时长选择取决于元素的"重要性"和"预期注意力":

场景 推荐时长 推荐缓动
按钮/小图标 150ms EmphasizedDecelerate
对话框/卡片 200ms EmphasizedDecelerate
菜单/下拉列表 250ms EmphasizedDecelerate
全屏转场 300ms EmphasizedDecelerate

缩放动画几乎总是用 EmphasizedDecelerate 缓动——快速启动、缓慢停止的效果能产生"有力但可控"的感觉。

常见使用场景

对话框弹出

void showDialog(QWidget* dialog) {
    // 初始状态:设置为缩小状态
    dialog->setGeometry(
        dialog->x() + dialog->width() * 0.1,
        dialog->y() + dialog->height() * 0.1,
        dialog->width() * 0.8,
        dialog->height() * 0.8
    );
    dialog->show();

    // 缩放到正常大小
    auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec);
    scaleAnim->setTargetWidget(dialog);
    scaleAnim->start(Direction::Forward);
}

按钮按下效果

class PressButton : public QPushButton {
public:
    void mousePressEvent(QMouseEvent* event) override {
        // 按下时缩小
        auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec);
        scaleAnim->setTargetWidget(this);
        scaleAnim->start(Direction::Backward);  // 缩小
        QPushButton::mousePressEvent(event);
    }

    void mouseReleaseEvent(QMouseEvent* event) override {
        // 释放时恢复
        auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec);
        scaleAnim->setTargetWidget(this);
        scaleAnim->start(Direction::Forward);  // 放大
        QPushButton::mouseReleaseEvent(event);
    }
};

菜单展开

void showMenu(QWidget* menu) {
    // 菜单从按钮位置"生长"出来
    // 需要将菜单的缩放中心设置为靠近按钮的位置
    // 这可以通过初始几何属性调整实现

    menu->show();

    auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec);
    scaleAnim->setTargetWidget(menu);
    scaleAnim->start();
}

组合动画:弹出 + 淡入

缩放和淡入的组合是"出现"效果的经典组合:

void showCard(QWidget* card) {
    card->show();

    auto scaleAnim = factory->getAnimation("md.animation.scaleUp");
    auto fadeAnim = factory->getAnimation("md.animation.fadeIn");

    scaleAnim->setTargetWidget(card);
    fadeAnim->setTargetWidget(card);

    scaleAnim->start();
    fadeAnim->start();
}

两个动画同步运行能产生"从无到有"的完整视觉体验——缩放负责空间维度,透明度负责视觉维度。

性能考量

缩放动画涉及 widget 几何属性的变化,可能触发父容器的重新布局:

  1. 避免在动画期间修改布局:如果 widget 的父容器使用复杂布局,动画期间禁用自动更新可以提升性能
  2. 固定尺寸 widget 效果更好:使用固定几何属性的 widget 动画更流畅
  3. 考虑淡入淡出替代:对于简单场景,纯透明度动画性能更好且视觉效果足够

缩放值范围

虽然 Material 推荐的缩放范围是 0.8~1.0(弹出)或 1.0~0.8(收起),但你可以根据需要调整:

// 更明显的弹出效果:从 0.5 开始
// 这需要修改动画配置或创建自定义动画

// 微妙的强调:从 0.95 开始
// 适合小元素或需要低调的场景

缩放到 1.0 以上(放大效果)在 Material Design 中比较少见,通常用于特殊交互如图片预览。这种场景下可能需要配合淡入淡出来避免突兀。

与布局管理器的交互

如果你的 widget 位于布局管理器中,缩放动画可能会与布局系统产生冲突:

// widget 在 QVBoxLayout 中
auto layout = new QVBoxLayout(parent);
layout->addWidget(myWidget);

// 缩放动画会改变 widget 的几何属性
// 布局管理器可能会尝试"纠正"这个变化
auto scaleAnim = std::make_unique<CFMaterialScaleAnimation>(&motionSpec);
scaleAnim->setTargetWidget(myWidget);
scaleAnim->start();  // 可能导致布局抖动

解决这个问题有几种方案:

  1. 动画期间临时移除布局约束
  2. 使用固定位置的 widget(不使用布局)
  3. 在布局中插入占位 widget 来吸收尺寸变化

常见问题

Q: 动画过程中 widget 闪烁

A: 可能是 WA_OpaquePaintEventWA_NoSystemBackground 属性设置不当。尝试设置 widget->setAttribute(Qt::WA_OpaquePaintEvent)

Q: 缩放后 widget 边缘模糊

A: 这是抗锯齿的正常现象。如果需要更清晰的边缘,可以启用 AA_EnableHighDpiScaling 或使用更高分辨率的资源。

Q: 动画结束后布局被破坏

A: 动画会修改 widget 的几何属性。如果需要恢复布局,监听 finished 信号并在其中调用 widget->setGeometry(originalGeometry)

相关文档