MdElevationController - Material 阴影控制器¶
MdElevationController 是 Material Design 3 高程系统的核心实现,负责管理控件的海拔级别和对应的阴影渲染。在 Material Design 中,高程(Elevation)是表达 UI 层级关系的关键手段,通过阴影和色调变化来传达"哪个元素在上"。我们选择自己实现,是因为 Qt 的图形系统默认不支持 Material 的多光源阴影模型和动画过渡。
Material Design 高程系统¶
Material Design 定义了 6 个标准海拔级别(0-5),每个级别对应不同的阴影参数:
| 级别 | 高程值 | 模糊半径 | 偏移量 | 不透明度 |
|---|---|---|---|---|
| Level 0 | 0dp | 0px | 0px | 0% |
| Level 1 | 1dp | 2px | 1px | 15% |
| Level 2 | 3dp | 4px | 2px | 20% |
| Level 3 | 6dp | 8px | 4px | 25% |
| Level 4 | 8dp | 12px | 6px | 30% |
| Level 5 | 12dp | 16px | 8px | 35% |
这些参数在 paramsForLevel() 中定义,阴影效果通过多层叠加实现近似模糊。
基本用法¶
MdElevationController 需要配合动画工厂使用,通常在控件构造函数中初始化:
#include "widget/material/base/elevation_controller.h"
using namespace cf::ui::widget::material;
class MyWidget : public QWidget {
public:
MyWidget(QWidget* parent = nullptr) : QWidget(parent) {
auto animationFactory = cf::WeakPtr<components::material::CFMaterialAnimationFactory>::DynamicCast(
Application::animationFactory()
);
// 创建高程控制器
m_elevation = new base::MdElevationController(animationFactory, this);
// 设置海拔级别
m_elevation->setElevation(2); // Level 2
}
private:
base::MdElevationController* m_elevation;
};
设置高程级别¶
直接使用 setElevation() 设置高程,这会立即更新阴影效果:
// 静态设置
m_elevation->setElevation(0); // 无阴影
m_elevation->setElevation(3); // 中等阴影
m_elevation->setElevation(5); // 最强阴影
高程值会被 clamp 在 [0, 5] 范围内,超出范围的值会自动截断。
动画过渡¶
对于需要动态高程变化的场景(如按钮按压),使用 animateTo() 方法:
#include "core/material/cfmaterial_motion.h"
// 按压时升高
void MyWidget::mousePressEvent(QMouseEvent* event) {
QWidget::mousePressEvent(event);
m_elevation->setPressed(true);
// 使用标准动画规格
auto spec = core::MotionSpec::standard();
m_elevation->animateTo(4, spec);
}
// 释放时恢复
void MyWidget::mouseReleaseEvent(QMouseEvent* event) {
QWidget::mouseReleaseEvent(event);
m_elevation->setPressed(false);
m_elevation->animateTo(2, core::MotionSpec::standard());
}
MotionSpec 定义了动画的缓动曲线和时长,使用 Material 标准值可确保动画感觉一致。
绘制阴影¶
阴影应该在 paintEvent 中最先绘制,这样它会出现在控件背景下方:
void MyWidget::paintEvent(QPaintEvent* event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
// Step 1: 先绘制阴影
QPainterPath shape = widgetShape();
m_elevation->paintShadow(&p, shape);
// Step 2: 绘制背景(会覆盖阴影的主体部分,只露出边缘)
p.fillPath(shape, backgroundColor());
// 其他绘制...
}
⚠️ 阴影必须先绘制,否则会覆盖控件内容。绘制顺序错了视觉效果会很奇怪。
光源角度¶
Material Design 假设光源来自左上方(约 15 度),这形成向右下方的阴影。可以通过 setLightSourceAngle() 调整:
// 默认值(光源来自左上方)
m_elevation->setLightSourceAngle(15.0f);
// 光源来自正上方
m_elevation->setLightSourceAngle(0.0f);
// 光源来自右侧
m_elevation->setLightSourceAngle(-30.0f);
角度正值表示光源从左侧来,阴影向右投射;负值表示光源从右侧来。这个参数影响阴影的水平偏移量。
按压效果¶
按压效果包含两部分:阴影变化和控件位移。控制器提供了 setPressed() 和 pressOffset() 来支持这个效果:
void MyWidget::mousePressEvent(QMouseEvent* event) {
m_elevation->setPressed(true);
update();
}
void MyWidget::paintEvent(QPaintEvent* event) {
QPainter p(this);
// 应用按压位移
float offset = m_elevation->pressOffset();
p.translate(0, offset);
// 正常绘制...
}
按压时阴影会缩小并靠近控件(约 50%),同时控件向下移动,产生"按下"的视觉反馈。
暗色主题支持¶
在暗色主题中,阴影效果不明显,Material Design 使用色调叠加来表示高程:
CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const {
// 返回混合后的表面颜色
}
使用方式:
bool isDark = theme->isDarkTheme();
CFColor surfaceColor = theme->getColor(MdColorRole::Surface);
CFColor primaryColor = theme->getColor(MdColorRole::Primary);
if (isDark) {
// 暗色主题使用色调叠加
backgroundColor = m_elevation->tonalOverlay(surfaceColor, primaryColor);
} else {
// 亮色主题使用阴影
backgroundColor = surfaceColor;
}
色调叠加量与高程级别成正比,级别越高叠加越多。
高程层级选择¶
选择合适的高程级别取决于控件在界面中的层级关系:
| 场景 | 推荐级别 |
|---|---|
| 平面控件(卡片、表单) | 0-1 |
| 悬浮按钮(FAB) | 3 |
| 对话框 | 4 |
| 底部抽屉 | 5 |
| 菜单、下拉框 | 2-3 |
保持相邻层级之间至少相差 1 级,确保视觉层次清晰。
性能考虑¶
阴影渲染涉及多层绘制和透明度混合,在低端设备上可能成为性能瓶颈。如果遇到性能问题:
- 降低高程级别(Level 1 和 2 的渲染开销较小)
- 全局禁用动画(阴影计算仍然进行,但没有插值开销)
- 在低端设备上禁用阴影(通过条件编译或运行时检测)
常见问题¶
阴影看起来"不对劲"通常是因为:
- 绘制顺序错误——阴影必须在背景之前绘制
- 控件没有设置
WA_TranslucentBackground属性(某些平台需要) - 设备像素比计算错误(高 DPI 屏幕上阴影会模糊)