动画与状态机原理
# 第 8 章 动画与状态机原理
本章定位:从"能动就行"到"懂插值引擎与渲染线程驱动"。QML 的动画不是 V4 JS 的
setInterval循环——它是 Render Thread 主导的插值引擎,独立于 GUI 线程。理解 PropertyAnimation 的 easing 曲线数学、Behavior 的自动拦截、SpringAnimation 的物理模型、State/Transition 状态机,以及在嵌入式上为什么transform动画帧率甩width/height动画几条街。
# 目录介绍
- 8.1 案例引入
- 8.2 引擎原理
- 8.3 属性动画体系
- 8.4 Behavior 动画
- 8.5 状态机
- 8.6 GPU 与 CPU
- 8.7 仪表盘动效
- 8.8 性能与陷阱
- 8.9 新手陷阱
- 8.10 训练题
- 8.11 思考题
- 8.12 速查表
# 8.1 案例引入
# 8.1.1 动画掉帧
某车机工程师在仪表盘上用 NumberAnimation on width 做机油进度条动画。在 ARM A53 + Mali-G31 板上运行时,FPS 从 60 一路跌到 12——比 ListView 的 rotation 事故还严重:
import QtQuick
Rectangle {
width: 800; height: 480
Rectangle {
id: oilBar
x: 100; y: 200
width: 10; height: 30; color: "#4CAF50" // ⚠️ width 动画
NumberAnimation on width {
from: 10; to: 400; duration: 2000
loops: Animation.Infinite
easing.type: Easing.InOutCubic
}
}
// 还有其他依赖 oilBar.width 的布局元素
Text {
anchors.left: oilBar.right; anchors.leftMargin: 10
anchors.verticalCenter: oilBar.verticalCenter
text: "机油压力"; font.pixelSize: 14
}
}
测得现象:
| 指标 | 数据 |
|---|---|
| FPS | 动画启动后从 60 稳定跌至 12 |
| CPU 占用 | 85%+(单核) |
| GPU 占用 | 仅 15% |
| 布局重算/帧 | 每帧强制触发 oilBar.width 变化 → 整个 anchors 链重新求解 |
疑惑:
- 动画不是在 Render Thread 独立跑的吗?为什么会卡 GUI 线程?
width动画和x动画在底层有什么本质区别?- 如果不改
width,怎么实现同样的视觉效果?
# 8.1.2 根因分析
每帧动画的 tick 过程:
Render Thread: 插值引擎计算当前 frame 的 width 值
→ width = lerp(10, 400, progress) // progress 由 easing 曲线给出
→ 通过 Sync 阶段把新 width 写入 QQuickItem
GUI Thread: width 变化
→ QQuickItem::setWidth(newWidth) ← 这里触发布局!
→ QQuickAnchors::update() ← anchors.left: oilBar.right 重新计算
→ Text 的 x 坐标被重新求值
→ 所有依赖 oilBar.width 的属性绑定全部重新求值
→ Scene Graph: oilBar + Text 都被标记为 dirty
→ 批处理被打断 → Draw Call 爆炸
修复方案——用 transform: Scale:
Rectangle {
id: oilBar
x: 100; y: 200
width: 400; height: 30; color: "#4CAF50" // ← 最终宽度预分配
transform: Scale {
origin.x: 0; origin.y: 0 // 从左侧开始缩放
xScale: 0.025 // 初始 10/400
}
NumberAnimation on transform.xScale {
from: 0.025; to: 1.0; duration: 2000
loops: Animation.Infinite
}
}
// Scale 直接影响 QSGTransformNode 的矩阵
// → 不触发 setProperty("width")
// → 不触发布局重算 → anchors 不动 → 批处理保持 → 稳定 60fps
# 8.1.3 三大问题
| 问题 | 在哪节回答 |
|---|---|
| 动画到底是在 Render Thread 还是 GUI 线程跑的?插值引擎怎么工作? | §8.2 |
width 动画为什么卡、x / scale 为什么不卡? | §8.6 |
| State / Transition 状态机在复杂交互场景下怎么替代一堆 if-else? | §8.5 |
读完本章你会亲手写一个 60fps 的车机仪表盘动效系统,并理解 Render Thread 动画与 GUI 线程绑定之间的协作边界。
# 8.2 引擎原理
# 8.2.1 插值引擎
QML 动画的核心不在 V4 JavaScript 里——在 Render Thread 里。这就是为什么 JS 线程卡死时动画还在动:
┌──────────────────────────┐ ┌──────────────────────────┐
│ GUI Thread │ │ Render Thread │
│ │ │ │
│ Animation 启动时: │ │ 每帧(16.6ms): │
│ QQmlAnimationTimer │ Sync │ QQuickAnimatorController │
│ 注册动画到 │ ────→ │ for each active anim: │
│ QUnifiedTimer │ │ progress = elapsed/dur │
│ │ │ eased = easing(prog) │
│ │ │ value = lerp(from,to, │
│ │ │ eased) │
│ │ │ setProperty(target, │
│ │ │ value) │
│ │ │ │
│ 收到 property change: │ ←──── │ (Sync 阶段传递) │
│ 触发 onChanged / │ │ │
│ 触发 binding 求值 │ │ │
└──────────────────────────┘ └──────────────────────────┘
关键:动画的插值计算在 Render Thread 完成,但 setProperty 必须通过 Sync 阶段写回 GUI 线程——这就是为什么 transform 动画比 width 动画快:transform 只写 QSGTransformNode,不经过复杂的 QQuickItem 布局链路。
动画与 Timer 的本质区别:
| 方式 | 驱动源 | 与 VSync 同步 | 依赖 GUI 线程 |
|---|---|---|---|
NumberAnimation | Render Thread | ✅ 自动对齐 | 仅在 Sync 阶段交互 |
Timer { interval: 16 } | Qt 事件循环 | ❌ 不准 | 完全依赖 GUI 线程 |
FrameAnimation | Render Thread | ✅ 严格同步 | 同 Animation |
# 8.2.2 缓动曲线
easing.type: Easing.InOutCubic 背后是一个 t → f(t) 的数学映射,把线性时间映射为非线性的属性变化:
常用 easing 曲线及数学定义:
| 类型 | f(t) 定义 | 视觉效果 |
|---|---|---|
Linear | t | 匀速 |
InQuad | t² | 加速启动 |
OutQuad | t(2-t) | 减速停止 |
InOutQuad | t<0.5 ? 2t² : -1+(4-2t)t | 加速→减速 |
InOutCubic | 三次方版本 | 更平滑的加速→减速 |
OutElastic | 弹簧式回弹 | "弹性"到达 |
OutBounce | 模拟弹跳 | 多次回弹 |
InOutBack | c3*t³ - c1*t² | "过冲后回正" |
自定义 easing 曲线——BezierSpline 四控制点:
NumberAnimation {
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.25, 0.1, 0.25, 1.0, 0.5, 0.5, 0.75, 0.9]
// 4 组 (c1x,c1y, c2x,c2y, c3x,c3y, c4x,c4y) 控制点
}
# 8.2.3 动画绑定协作
当动画驱动的属性同时被 QML 绑定依赖时:
Rectangle {
id: ball
x: 0
NumberAnimation on x { to: 300; duration: 1000; running: true }
Rectangle {
id: shadow
x: ball.x - 10 // ← 绑定依赖 ball.x
opacity: ball.x / 300 // ← 绑定依赖 ball.x
color: ball.x > 150 ? "gray" : "black"
}
}
协作流程:
每帧动画 tick:
Render Thread: ball.x = lerp(0, 300, easedProgress)
→ Sync 阶段: setProperty(ball, "x", newX)
→ GUI Thread: 检测到 ball.x 的 QQmlNotifier 变化
→ shadow.x 绑定被标记 dirty
→ shadow.opacity 绑定被标记 dirty
→ shadow.color 绑定被标记 dirty
→ processPendingBindings()
→ shadow.x = ball.x - 10(求值一次)
→ shadow.opacity = ball.x / 300(求值一次)
→ shadow.color = ball.x > 150 ? "gray" : "black"(求值一次)
→ 三个脏标记统一处理 → 一次 Sync → 高效
# 8.3 属性动画体系
# 8.3.1 三种基础动画
// NumberAnimation — 适用于 int / real 属性
NumberAnimation {
target: rect; property: "opacity"
from: 1.0; to: 0.0; duration: 500
easing.type: Easing.OutQuad
}
// ColorAnimation — 适用于 color 属性(在颜色空间中插值)
ColorAnimation {
target: rect; property: "color"
from: "#FF0000"; to: "#0000FF"; duration: 1000
}
// 插值路径: 红色 → 紫色 → 蓝色(RGB 空间线性插值)
// RotationAnimation — 适用于 rotation 属性(支持方向控制)
RotationAnimation {
target: spinner; property: "rotation"
from: 0; to: 360; duration: 2000
direction: RotationAnimation.Clockwise
loops: Animation.Infinite
}
声明式 vs 命令式:
// 声明式——写在目标对象的 {} 内(自动启动)
Rectangle {
id: ball
NumberAnimation on x { to: 200; duration: 1000; running: true }
}
// 命令式——单独创建 Animation 对象,控制 target/property
Rectangle { id: ball }
NumberAnimation {
id: ballAnim
target: ball; property: "x"
to: 200; duration: 1000
}
Button { onClicked: ballAnim.start() } // 手动控制启动
# 8.3.2 串行与并行
// 串行动画——逐个执行
SequentialAnimation {
id: seq
loops: Animation.Infinite
NumberAnimation { target: rect; property: "x"; to: 300; duration: 800 }
PauseAnimation { duration: 200 } // 暂停
NumberAnimation { target: rect; property: "y"; to: 200; duration: 600 }
NumberAnimation { target: rect; property: "opacity"; to: 0; duration: 400 }
}
// 并行动画——同时执行
ParallelAnimation {
NumberAnimation { target: ball; property: "x"; to: 300; duration: 1000 }
NumberAnimation { target: ball; property: "y"; to: 200; duration: 1000 }
NumberAnimation { target: ball; property: "scale"; to: 1.5; duration: 1000 }
}
// 嵌套组合
SequentialAnimation {
ParallelAnimation {
NumberAnimation { target: btn; property: "scale"; to: 1.2; duration: 150 }
NumberAnimation { target: btn; property: "opacity"; to: 0.8; duration: 150 }
}
ParallelAnimation {
NumberAnimation { target: btn; property: "scale"; to: 1.0; duration: 150 }
NumberAnimation { target: btn; property: "opacity"; to: 1.0; duration: 150 }
}
}
# 8.3.3 命令式控制
NumberAnimation {
id: anim
target: rect; property: "x"; to: 300; duration: 2000
}
Button { text: "▶"; onClicked: anim.start() }
Button { text: "⏸"; onClicked: anim.pause() }
Button { text: "▶▶"; onClicked: anim.resume() }
Button { text: "⏹"; onClicked: anim.stop() } // 停在原位
Button { text: "⏮"; onClicked: anim.complete() } // 直接跳到最终值
# 8.4 Behavior 动画
# 8.4.1 拦截机制
Rectangle {
id: rect
width: 100; height: 100; color: "steelblue"
Behavior on x {
NumberAnimation { duration: 300; easing.type: Easing.OutQuad }
}
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// 任何对 rect.x 的赋值——
// rect.x = 200、x 绑定求值、甚至 C++ setProperty("x", 200)
// → 全部被 Behavior 拦截
// → 不直接写值,而是启动一个 300ms 的动画从当前值缓动到目标值
Behavior 的底层实现——它在 QQmlProperty 的 write 路径上注册了一个拦截器:
rect.x = 200
→ QQmlProperty::write(rect, "x", 200)
→ 检测到 Behavior 注册在 "x" 上
→ Behavior::onPropertyChanged(newValue=200)
→ 读取当前值: currentX = rect.x
→ animation.from = currentX
→ animation.to = 200
→ animation.start()
→ 动画每帧 tick: rect.x = lerp(currentX, 200, progress)
Behavior vs 声明式动画:
| 维度 | NumberAnimation on x {} | Behavior on x {} |
|---|---|---|
| 触发方式 | 启动时运行一次 | 每次 x 变化都触发 |
| 控制力 | 完全由动画参数决定 | 外部随意改 x → 自动动画过渡 |
| 适合场景 | 固定动画序列 | 交互驱动的平滑过渡 |
# 8.4.2 多属性组合
Rectangle {
id: card; width: 200; height: 150; color: "white"
Behavior on x { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } }
Behavior on y { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } }
Behavior on scale { NumberAnimation { duration: 200; easing.type: Easing.OutBack } }
Behavior on color { ColorAnimation { duration: 400 } }
MouseArea {
anchors.fill: parent
onClicked: {
card.x = Math.random() * 400
card.y = Math.random() * 300
card.scale = 1.0 + Math.random() * 0.5
card.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
// 四个属性同时变化 → 四个动画同时启动 → 各自的 Behavior 独立运作
}
}
}
# 8.5 状态机
# 8.5.1 声明式状态
Rectangle {
id: panel
width: 60; height: 400; color: "#333"
states: [
State {
name: "collapsed"
PropertyChanges { target: panel; width: 60; color: "#333" }
PropertyChanges { target: expandBtn; rotation: 0 }
},
State {
name: "expanded"
PropertyChanges { target: panel; width: 300; color: "#555" }
PropertyChanges { target: expandBtn; rotation: 180 }
}
]
state: "collapsed" // 初始状态
Rectangle {
id: expandBtn
anchors { right: parent.right; verticalCenter: parent.verticalCenter; margins: 8 }
width: 30; height: 30; radius: 15; color: "#888"
MouseArea {
anchors.fill: parent
onClicked: panel.state = (panel.state === "collapsed") ? "expanded" : "collapsed"
}
}
}
State 的 when 条件——自动状态切换:
State {
name: "warning"
when: speed > 120 // ← 条件满足时自动切换到这个 state
PropertyChanges { target: bg; color: "red" }
}
State {
name: "normal"
when: speed <= 120
PropertyChanges { target: bg; color: "green" }
}
# 8.5.2 过渡动画
transitions: [
Transition {
from: "collapsed"; to: "expanded"
ParallelAnimation {
NumberAnimation {
target: panel; property: "width"
duration: 400; easing.type: Easing.OutCubic
}
ColorAnimation {
target: panel; property: "color"
duration: 400
}
RotationAnimation {
target: expandBtn; property: "rotation"
duration: 300
}
}
},
Transition {
from: "expanded"; to: "collapsed"
NumberAnimation {
target: panel; property: "width"
duration: 300; easing.type: Easing.InCubic // 收起更快
}
}
]
Transition 的可逆性——用 reversible: true 让过渡反向播放:
Transition {
from: "*"; to: "*" // 任意状态切换都走这个过渡
reversible: true // 如果中途切换,反向播放当前动画
NumberAnimation {
properties: "width,opacity"; duration: 400
}
}
# 8.5.3 折叠面板
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 400; height: 600; visible: true
Rectangle {
id: sidebar
width: 60; height: parent.height
color: "#1a1a2e"; clip: true
states: [
State {
name: "closed"
PropertyChanges { target: sidebar; width: 60 }
PropertyChanges { target: menuContent; opacity: 0 }
PropertyChanges { target: toggleBtn; rotation: 0 }
},
State {
name: "open"
PropertyChanges { target: sidebar; width: 250 }
PropertyChanges { target: menuContent; opacity: 1 }
PropertyChanges { target: toggleBtn; rotation: 180 }
}
]
state: "closed"
transitions: Transition {
from: "*"; to: "*"; reversible: true
ParallelAnimation {
NumberAnimation {
target: sidebar; property: "width"
duration: 350; easing.type: Easing.InOutCubic
}
NumberAnimation {
target: menuContent; property: "opacity"
duration: 250
}
RotationAnimation {
target: toggleBtn; property: "rotation"
duration: 300; direction: RotationAnimation.Clockwise
}
}
}
Column {
id: menuContent
anchors { fill: parent; margins: 12 }
spacing: 8
Text { text: "菜单"; color: "white"; font.pixelSize: 18; font.bold: true }
Repeater {
model: ["仪表盘", "导航", "音乐", "设置", "空调"]
Text { text: modelData; color: "#aaa"; font.pixelSize: 14 }
}
}
Rectangle {
id: toggleBtn
anchors { right: parent.right; top: parent.top; margins: 12 }
width: 30; height: 30; radius: 15; color: "#555"
Text { anchors.centerIn: parent; text: ">"; color: "white" }
MouseArea {
anchors.fill: parent
onClicked: sidebar.state = (sidebar.state === "open") ? "closed" : "open"
}
}
}
}
案例知识融合:本案例用 State/Transition 替换了传统的"if-else 手动设宽+手动启动动画"模式——①四个 PropertyChanges 声明了 closed/open 两个状态下的所有属性值;②一个 Transition from "*" to "*" 覆盖所有状态切换的过渡动画;③reversible: true 让正向/反向过渡自动对称;④调用方只需 sidebar.state = "open" ——一行。
思考题:
- 如果中途用户连续点击两次按钮(open → closed 切换刚启动又被切回 open),
reversible: true会怎么处理?动画会"弹回来"吗? width动画在这里用于折叠面板。按 §8.1 的铁律,它是不是该用transform: Scale替代?权衡"视觉效果"和"性能",你如何决策?
# 8.6 GPU 与 CPU
# 8.6.1 属性路径分类
| 属性 | 底层路径 | FPS 稳定性 | 触发布局重算 | 触发批处理打断 | 推荐 |
|---|---|---|---|---|---|
x / y | QSGTransformNode 矩阵 | ✅ 60fps | ❌ | ❌ | ✅ |
rotation | QSGTransformNode 矩阵 | ✅ 60fps | ❌ | ❌ | ✅ |
scale | QSGTransformNode 矩阵 | ✅ 60fps | ❌ | ❌ | ✅ |
opacity | QSGOpacityNode | ⚠️ 40-60fps | ❌ | ✅ 每个独立 | ⚠️ |
width / height | QQuickItem::setWidth | ❌ < 30fps | ✅ | ✅ | ❌ |
color | QSGGeometryNode 顶点属性 | ✅ 60fps | ❌ | ❌ | ✅ |
anchors.* | 约束求解器 | ❌ < 20fps | ✅ | ✅ | ❌ |
"width 动画"的替代方案:
// ❌ width 动画——触发布局重算
Rectangle {
NumberAnimation on width { to: 300; duration: 1000 }
}
// ✅ scale 动画——仅 GPU 变换矩阵
Rectangle {
width: 300 // 预分配最终宽度
transform: Scale { origin.x: 0; xScale: 0.1 }
NumberAnimation on transform.xScale { to: 1.0; duration: 1000 }
}
// ✅ x 动画——仅平移
Rectangle {
x: -width // 从屏幕外移入
NumberAnimation on x { to: 0; duration: 500 }
}
# 8.6.2 弹性动画
SpringAnimation 模拟物理弹簧的运动——比 easing 曲线更自然:
SpringAnimation {
target: ball; property: "y"
from: 0; to: 300
spring: 2.0 // 弹簧刚度(越大振动越快)
damping: 0.2 // 阻尼系数(越小弹越久)
epsilon: 0.01 // 停止阈值(振幅小于此值则停止)
duration: 2000 // 最大时长
}
物理模型(Hooke's Law + 阻尼):
加速度 a = -k(spring) × 位移 + -c(damping) × 速度
每帧: velocity += a * dt
position += velocity * dt
当 |velocity| < epsilon 且 |target - position| < epsilon → 动画结束
# 8.6.3 Shader 动画
对于更复杂的视觉效果(模糊、颜色矩阵、扭曲),用 ShaderEffect + 动画驱动的 uniform:
ShaderEffect {
width: 200; height: 200
property real blurRadius: 0
property variant source: someImage
fragmentShader: "
uniform sampler2D source;
uniform float blurRadius;
varying vec2 qt_TexCoord0;
void main() {
// 高斯模糊实现(简化)
vec4 color = texture2D(source, qt_TexCoord0);
gl_FragColor = color * (1.0 - blurRadius * 0.1);
}"
NumberAnimation on blurRadius {
from: 0; to: 10; duration: 2000; loops: Animation.Infinite
}
}
# 8.7 仪表盘动效
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 800; height: 480; visible: true; title: "仪表盘"
color: "#0d0d1a"
property real speed: 0
property real rpm: 0
property real fuel: 80
// ───── 速度条(GPU 加速 scale 动画) ─────
Rectangle {
id: speedBar
x: 50; y: 50
width: 300; height: 40; radius: 8; color: "#333"
Rectangle {
width: parent.width; height: parent.height; radius: parent.radius
color: speed > 120 ? "#FF5252" : "#4CAF50"
transform: Scale { origin.x: 0; xScale: 0 }
NumberAnimation on transform.xScale {
id: speedAnim
to: speed / 200; duration: 300; easing.type: Easing.OutCubic
}
Behavior on color { ColorAnimation { duration: 400 } }
}
Text {
anchors.centerIn: parent
text: speed.toFixed(0) + " km/h"
color: "white"; font.pixelSize: 18; font.bold: true
}
}
// ───── 转速指针(rotation 动画) ─────
Rectangle {
id: rpmNeedle
x: 450; y: 50
width: 4; height: 150
color: "#FF9800"
transformOrigin: Item.Bottom
rotation: -135 // 起始角度
NumberAnimation on rotation {
id: rpmAnim
to: -135 + rpm / 7000 * 270; duration: 200; easing.type: Easing.OutQuad
}
}
// ───── 状态机:低油量警告 ─────
Rectangle {
id: fuelIndicator
anchors { bottom: parent.bottom; left: parent.left; margins: 20 }
width: 100; height: 30; radius: 6
states: [
State {
name: "ok"; when: fuel >= 15
PropertyChanges { target: fuelIndicator; color: "#4CAF50" }
PropertyChanges { target: fuelText; text: "燃料 OK" }
},
State {
name: "low"; when: fuel < 15 && fuel > 5
PropertyChanges { target: fuelIndicator; color: "#FF9800" }
PropertyChanges { target: fuelText; text: "燃料偏低" }
},
State {
name: "critical"; when: fuel <= 5
PropertyChanges { target: fuelIndicator; color: "#FF5252" }
PropertyChanges { target: fuelText; text: "⚠ 缺油" }
}
]
transitions: Transition {
ColorAnimation { duration: 500 }
}
Text { id: fuelText; anchors.centerIn: parent; color: "white"; font.pixelSize: 12 }
}
// ───── 模拟数据 ─────
Timer {
interval: 50; repeat: true; running: true
onTriggered: {
speed = 80 + Math.sin(Date.now() / 1500) * 60
rpm = 3000 + Math.sin(Date.now() / 1000) * 2500
fuel -= 0.01
}
}
}
案例知识融合:本案例集成了本章全部核心——①速度条用 transform: Scale 避免 width 动画;②转速指针用 rotation 动画天然 GPU 加速;③燃料指示器用 State/Transition 状态机替代 if-else;④Behavior on color 自动响应速度/燃料的颜色变化;⑤所有动画在 Render Thread 驱动、与 VSync 对齐。
跨章知识串连——这套仪表盘动效背后的每个动画决策都对应一条底层原理:
| 决策 | 背后原理 | 参考章节 |
|---|---|---|
用 transform: Scale 而非 width 动画 | 布局线路 vs 变换矩阵:width 触发 anchors 求解 + 布局链,Scale 只改 QSGTransformNode 矩阵 | §2.3 渲染管线 + §5.4 锚布局 |
rotation 直接映射到 QSGTransformNode | Item.rotation → QSGTransformNode 矩阵:无 CPU 光栅化 | §11.2.3 节点变换 |
Behavior on color 自动拦截 setProperty | Behavior 拦截机制:不改绑定,只在写入时插入 ColorAnimation | §8.4.1 拦截机制 |
PropertyChanges 声明式状态 | 状态机 vs 命令式:QQmlStateGroup 内部维护属性快照,减少绑定重建 | §8.5.1 声明式状态 |
| 60fps 稳定不掉帧 | Render Thread 独立时钟:GUI 线程忙也不影响动画插值 | §2.4.1 双线程模型 |
动画期间 speed 属性有 NOTIFY | Q_PROPERTY + emit → 依赖收集通知 → 动画目标值自动跟进 | §10.2.2 通知机制 |
探索性思考——为什么 QML 的动画能"独立于 JS 线程"?
反问:如果 NumberAnimation 在 V4 JS 里跑 setInterval 循环,会发生什么?
- JS 是单线程——一次 GC 卡 200ms → 动画停 200ms
- JS 到 C++ 的每次调用都有开销——60fps × 每帧插值 → 每秒 60 次 JS→C++ 跳转
setInterval的时钟精度依赖 event loop——GUI 线程忙时定时器会漂移
Qt 的解决方案:把动画搬到 Render Thread 里,用 QAbstractAnimation::updateCurrentTime(qint64) 在 vsync 回调里推进——JS 只负责创建/销毁动画对象,插值全在 C++ 里。这就是为什么本例中 JS 线程被阻塞时动画还能继续。
# 8.8 性能与陷阱
| 场景 | 问题 | 优化 |
|---|---|---|
10 个动画同时跑 opacity | 10 个独立 QSGOpacityNode → 批处理打断 | 合到一个 opacity 驱动的 Item 上 |
width / height 动画 | 每帧触发布局重算 | 换 transform: Scale |
SequentialAnimation 内有 PauseAnimation | 暂停期间 Render Thread 空转 | 用时长大致控制节奏,非精确暂停 |
| SpringAnimation 不设 epsilon | 弹簧永不停止—CPU 持续计算 | 设 epsilon: 0.01 |
PropertyChanges 数量过多 | 切换 state 时批量属性变更 | 合并相关属性到一个 PropertyChanges 对象 |
| Behavior 覆盖声明式绑定 | 绑定求值结果被 Behavior 拦截后二次动画化 | 确认 Behavior 和目标属性不存在循环依赖 |
# 8.9 新手陷阱
| # | 陷阱 | 说明 | 修复 |
|---|---|---|---|
| 1 | width / height 动画 | 每帧触发布局重算 + 批处理全打断 | 换 transform: Scale 或 x / y 动画 |
| 2 | 忘记 running: true | 声明式动画不写 running: true 默认不启动 | 显式设置 |
| 3 | State when 条件冲突 | 两个 state 的 when 同时满足 → 行为不确定 | 确保 when 条件互斥 |
| 4 | SpringAnimation 无 epsilon | 弹簧永不收敛 → CPU 持续计算 | 设 epsilon: 0.01 |
| 5 | Behavior 与声明式动画同用 | 两者都写 on x → 冲突 | 二选一 |
# 8.10 训练题
训练题:修复 width 动画卡顿
将下面的 width 动画改为 GPU 加速的版本:
Rectangle {
id: bar
width: 5; height: 20; color: "steelblue"
NumberAnimation on width { to: 200; duration: 1500; loops: Animation.Infinite }
}
参考答案
Rectangle {
id: bar
width: 200; height: 20; color: "steelblue"
transform: Scale { origin.x: 0; xScale: 0.025 }
NumberAnimation on transform.xScale { to: 1.0; duration: 1500; loops: Animation.Infinite }
}
# 8.11 思考题
动画在 Render Thread 为何不直接 setProperty? 动画在 Render Thread 计算插值后,为什么不能直接在 Render Thread 里调
QQuickItem::setProperty?非要通过 Sync 阶段传回 GUI 线程?这个设计有什么深层原因?FrameAnimation vs NumberAnimation with duration: 16:两者都能产生"每帧触发"的效果——区别在哪?什么场景下必须用
FrameAnimation?State 状态机 in 嵌入式:车机上可能有 20+ 个 State(正常/警告/严重/诊断/夜间/...)。全用
when条件自动切换,和全用state = "xxx"手动切换——哪种更适合车载 HMI?为什么?
# 8.12 速查表
| 概念 | 一句话 |
|---|---|
| NumberAnimation | 对 int/real 属性插值——from/to/duration/easing |
| ColorAnimation | 颜色空间插值——RGB 线性渐变 |
| RotationAnimation | 旋转专用——支持 Clockwise/Counterclockwise/Shortest |
| SequentialAnimation | 串行动画组——逐个执行,可内嵌 PauseAnimation |
| ParallelAnimation | 并行动画组——所有子动画同时启动 |
| Behavior | 拦截 setProperty——属性变化时自动启动过渡动画 |
| State | 声明式状态——PropertyChanges 开关属性集合 |
| Transition | 状态切换的过渡动画——from/to/reversible |
| SpringAnimation | 物理弹簧动画——spring/damping/epsilon |
| Easing | 缓动曲线 40+ 种——控制插值节奏 |
| FrameAnimation | VSync 同步的帧回调——每帧执行一次 |
| transform: Scale | width 动画的 GPU 替代——零布局开销 |
| Render Thread | 动画插值计算所在的线程——与 VSync 对齐、独立于 GUI |
核心哲学:
QML 动画 = Render Thread 插值引擎 + 属性变换矩阵。
用 transform 做动画 → 仅 GPU 矩阵计算 → 60fps。
用 width 做动画 → 整树布局重算 → 12fps。
不是动画本身慢——是选错了被动画的属性。