CFMaterialSlideAnimation - Material 滑动动画¶
CFMaterialSlideAnimation 实现了 Material Design 3 规范中的位移动画,控制 widget 在屏幕空间中的位置变化。滑动动画是 Material Design 中最重要的过渡方式之一——它利用空间位置的变化来建立元素之间的视觉关联,让用户理解"这个东西从哪里来、到哪里去"。
在 Material Design 3 中的定位¶
在 Material 的运动语言里,滑动代表了"元素从边界进入或离开"。不同方向的滑动有不同的语义:
| 方向 | 典型用途 | 语义 |
|---|---|---|
| Up | 底部sheet、键盘升起、 Snackbar | 从下方边界进入 |
| Down | 下拉刷新、关闭底部sheet | 向下方边界退出 |
| Left | 侧边菜单收起、向右切换内容 | 向左边界退出/进入 |
| Right | 侧边菜单展开、向左切换内容 | 从右边界进入/退出 |
这个语义映射不是绝对的——具体用哪个方向取决于你的 UI 布局和导航逻辑,但保持一致性很重要。
基本用法¶
滑动动画需要在构造时指定方向,因为方向决定了位移的正负:
#include "ui/components/material/cfmaterial_slide_animation.h"
using namespace cf::ui::components::material;
// 创建一个向上滑动的动画(widget 从下方移入)
auto slideAnim = std::make_unique<CFMaterialSlideAnimation>(
&motionSpec,
SlideDirection::Up, // 滑动方向
this
);
// 设置滑动距离(像素)
slideAnim->setDistance(100.0f);
// 设置目标并启动
slideAnim->setTargetWidget(myCard);
slideAnim->start(Direction::Forward);
方向枚举的命名可能会让你困惑——SlideDirection::Up 表示 widget 向上移动,视觉上是"从下方滑入"。反过来想:枚举值描述的是 widget 移动的轨迹方向,而不是来源方向。
滑动距离¶
setDistance() 控制动画的位移量,单位是像素。默认值是 100px,但这个值在不同屏幕尺寸下可能不合适。我们建议根据实际使用场景动态计算:
// 列表项从右侧滑入,距离为父容器宽度的 30%
float offset = parentWidget->width() * 0.3f;
slideAnim->setDistance(offset);
// 底部 sheet 从屏幕底部滑入,距离为父容器高度
slideAnim->setDistance(parentWidget->height());
// Snackbar 从底部轻微上浮
slideAnim->setDistance(50.0f);
⚠️ 距离值应该是正数。方向由 SlideDirection 控制,不需要用负距离来表示反向移动。
原始位置保存¶
动画会在启动时保存 widget 的原始位置,并在动画停止时恢复。这个设计的行为是:
// 假设 widget 当前在 (100, 100)
slideAnim->setTargetWidget(widget);
slideAnim->start(Direction::Forward);
// ... 动画运行中,widget 位置被修改 ...
slideAnim->stop(); // widget 回到 (100, 100)
但如果你在动画过程中手动移动了 widget,动画停止时恢复的位置仍然是启动时的原始位置,这可能导致视觉跳跃。如果需要在动画结束后将 widget 定位到新位置,应该在 finished 信号中处理:
connect(slideAnim.get(), &ICFAbstractAnimation::finished, this, [widget]() {
// 动画结束后,手动设置到目标位置
widget->move(200, 100);
});
方向详解¶
四个方向的位移计算逻辑如下:
// SlideDirection::Up: widget 向上移动(从下方进入)
// 位移应用到 y 轴,offset 为负值
offset = QPoint(0, -distance)
// SlideDirection::Down: widget 向下移动(从上方进入)
// 位移应用到 y 轴,offset 为正值
offset = QPoint(0, distance)
// SlideDirection::Left: widget 向左移动(从右侧进入)
// 位移应用到 x 轴,offset 为负值
offset = QPoint(-distance, 0)
// SlideDirection::Right: widget 向右移动(从左侧进入)
// 位移应用到 x 轴,offset 为正值
offset = QPoint(distance, 0)
理解这个逻辑有助于你在调试时判断问题方向——如果动画朝反方向移动,大概率是方向枚举选错了。
时序参数¶
滑动动画通常使用比淡入淡出更长的时长,因为位移变化的视觉冲击力更大:
| 场景 | 推荐时长 | 推荐缓动 |
|---|---|---|
| 小元素滑动(按钮、图标) | 200ms | EmphasizedDecelerate |
| 列表项滑入 | 250ms | EmphasizedDecelerate |
| 底部 Sheet / 侧边栏 | 300ms | EmphasizedDecelerate |
| 全屏页面切换 | 350ms | EmphasizedDecelerate |
工厂会根据你选择的动画 token 自动应用这些参数,一般不需要手动干预。
常见使用场景¶
底部 Sheet 弹出¶
void showBottomSheet(QWidget* sheet, QWidget* parent) {
// 初始位置:放在父容器底部之外
int startY = parent->height();
sheet->move(0, startY);
sheet->show();
// 向上滑动到最终位置
auto slideAnim = std::make_unique<CFMaterialSlideAnimation>(
&motionSpec, SlideDirection::Up);
slideAnim->setDistance(parent->height());
slideAnim->setTargetWidget(sheet);
slideAnim->start();
}
侧边菜单滑入¶
void showSideMenu(QWidget* menu, QWidget* parent) {
// 从右侧滑入
menu->setParent(parent);
menu->move(parent->width(), 0);
menu->show();
auto slideAnim = std::make_unique<CFMaterialSlideAnimation>(
&motionSpec, SlideDirection::Left);
slideAnim->setDistance(parent->width());
slideAnim->setTargetWidget(menu);
slideAnim->start();
}
列表项交错动画¶
void showListItems(const QList<QWidget*>& items) {
for (int i = 0; i < items.size(); ++i) {
auto slideAnim = std::make_unique<CFMaterialSlideAnimation>(
&motionSpec, SlideDirection::Up);
// 设置交错延迟
slideAnim->setDelay(i * 50); // 每项延迟 50ms
slideAnim->setDistance(30.0f);
slideAnim->setTargetWidget(items[i]);
slideAnim->start();
}
}
下拉刷新¶
void onPullDown(float offset) {
// offset 是下拉的距离,正值
auto slideAnim = std::make_unique<CFMaterialSlideAnimation>(
&motionSpec, SlideDirection::Up);
slideAnim->setDistance(offset);
// 反向播放:回到原位
slideAnim->start(Direction::Backward);
}
性能优化建议¶
滑动动画涉及 widget 几何属性的变化,相对淡入淡出有更高的渲染开销:
- 避免同时动画大量 widget:如果一个列表有上百项同时滑入,考虑分批或使用交错延迟
- 硬件加速:确保 widget 的
WA_TranslucentBackground或WA_NoSystemBackground属性设置正确 - 简化布局:动画期间避免复杂的布局计算,可以考虑用固定几何属性
组合使用¶
滑动动画常与其他动画组合以产生更丰富的效果:
// 从右下方滑入,同时淡入
auto slideAnim = factory->getAnimation("md.animation.slideInRight");
auto fadeAnim = factory->getAnimation("md.animation.fadeIn");
slideAnim->setTargetWidget(widget);
fadeAnim->setTargetWidget(widget);
slideAnim->start();
fadeAnim->start();
这种组合在卡片、对话框等"重要元素"出现时特别有效。
常见问题¶
Q: 动画结束后 widget 跳回原位置
A: 这是因为动画停止时会恢复原始位置。如果需要保持最终位置,监听 finished 信号并在其中手动设置 widget 位置。
Q: 滑动方向与预期相反
A: 检查 SlideDirection 枚举值。枚举表示 widget 移动的方向,不是来源方向。Up = 向上移动 = 从下方进入。
Q: 动画过程中 widget 闪烁
A: 可能是布局系统在干扰。尝试在动画期间暂停布局更新,或者使用固定位置而不是依赖布局管理器。