StateMachine - Material 状态机
StateMachine 是 Material Design 3 交互状态的核心管理组件。它负责处理控件的各种交互状态(悬停、按下、焦点、禁用等),并按照 Material 规范驱动状态层的透明度动画。我们选择单独实现这个组件,是因为 Qt 的状态系统对 Material 的状态层动画支持不够友好,需要更精细的控制。
状态定义
Material Design 3 定义了以下几种交互状态,每个状态对应不同的状态层透明度:
cpp
enum class State {
StateNormal = 0x00, // 默认状态
StateHovered = 0x01, // 鼠标悬停
StatePressed = 0x02, // 鼠标按下
StateFocused = 0x04, // 键盘焦点
StateDisabled = 0x08, // 禁用状态
StateChecked = 0x10, // 选中状态(如 ToggleButton)
StateDragged = 0x20, // 拖拽状态
};
```bash
这些状态可以组合存在(比如同时有焦点和悬停),状态机内部通过位运算处理优先级。
## 透明度规范
状态层的透明度直接影响交互反馈的"分量感"。Material Design 3 的规范如下:
| 状态 | 透明度 | 说明 |
|------|--------|------|
| Normal | 0.00 | 无状态层 |
| Hovered | 0.08 | 轻微的视觉反馈 |
| Pressed | 0.12 | 明确的按下反馈 |
| Focused | 0.12 | 与按下相同 |
| Dragged | 0.16 | 最强的交互状态 |
| Disabled | 0.00 | 禁用时无状态层 |
| Checked | 0.08 | 与悬停相同 |
当多个状态同时存在时,按以下优先级取最高值:`Disabled > Pressed > Dragged > Focused > Hovered > Normal`。这个顺序在 `targetOpacityForState()` 里实现,改动顺序会破坏交互的一致性。
## 基本用法
`StateMachine` 需要配合动画工厂使用,通常在控件的构造函数中初始化:
```cpp
#include "widget/material/base/state_machine.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_stateMachine = new base::StateMachine(animationFactory, this);
// 监听状态变化
connect(m_stateMachine, &base::StateMachine::stateLayerOpacityChanged,
this, QOverload<>::of(&MyWidget::update));
}
private:
base::StateMachine* m_stateMachine;
};
```text
## 事件处理
状态机提供了一系列事件处理方法,需要将 Qt 的事件转发给它:
```cpp
void MyWidget::enterEvent(QEnterEvent* event) {
QWidget::enterEvent(event);
m_stateMachine->onHoverEnter();
update();
}
void MyWidget::leaveEvent(QEvent* event) {
QWidget::leaveEvent(event);
m_stateMachine->onHoverLeave();
update();
}
void MyWidget::mousePressEvent(QMouseEvent* event) {
QWidget::mousePressEvent(event);
m_stateMachine->onPress(event->pos());
update();
}
void MyWidget::mouseReleaseEvent(QMouseEvent* event) {
QWidget::mouseReleaseEvent(event);
m_stateMachine->onRelease();
update();
}
void MyWidget::focusInEvent(QFocusEvent* event) {
QWidget::focusInEvent(event);
m_stateMachine->onFocusIn();
update();
}
void MyWidget::focusOutEvent(QFocusEvent* event) {
QWidget::focusOutEvent(event);
m_stateMachine->onFocusOut();
update();
}
```text
禁用状态的监听稍微特殊,因为它通过 `changeEvent` 触发:
```cpp
void MyWidget::changeEvent(QEvent* event) {
QWidget::changeEvent(event);
if (event->type() == QEvent::EnabledChange) {
if (isEnabled()) {
m_stateMachine->onEnable();
} else {
m_stateMachine->onDisable();
}
update();
}
}
```text
## 绘制状态层
状态层是一个半透明的叠加层,绘制在控件背景之上、内容之下。在 `paintEvent` 中使用状态机提供的透明度值:
```cpp
void MyWidget::paintEvent(QPaintEvent* event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
// 先绘制背景
p.fillPath(shape, backgroundColor());
// 绘制状态层
float opacity = m_stateMachine->stateLayerOpacity();
if (opacity > 0.0f) {
CFColor stateColor = labelColor(); // 通常与文本颜色相同
QColor layerColor = stateColor.native_color();
layerColor.setAlphaF(layerColor.alphaF() * opacity);
p.fillPath(shape, layerColor);
}
// 再绘制其他内容...
}
```text
## 选中状态
对于有选中状态的控件(如 RadioButton、CheckBox、ToggleButton),还需要监听选中状态的变化:
```cpp
void MyWidget::setChecked(bool checked) {
if (m_checked != checked) {
m_checked = checked;
m_stateMachine->onCheckedChanged(checked);
update();
}
}
```text
⚠️ 选中状态(Checked)只是一种"持久化"的悬停状态,它不应该阻止其他交互状态的叠加。
## 禁用状态保护
状态机内部已经对禁用状态做了保护——在禁用状态下,悬停、按下、焦点等交互事件不会生效。这确保了禁用控件的视觉一致性,不需要在控件层再做额外判断。
## 动画性能
状态机使用动画工厂创建淡入淡出动画,当全局动画被禁用时,会直接设置透明度值而跳过动画。这对于性能敏感的场景很有用:
```cpp
// 在应用层面禁用动画
auto factory = Application::animationFactory();
if (factory) {
factory->setEnabledAll(false);
}
```text
## 相关文档
- [Button - Material 按钮](./button.md)
- [RippleHelper - 水波纹效果](../base/ripple_helper.md)
- [MdElevationController - 阴影控制器](../base/elevation_controller.md)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180