编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
        • 8.1 案例引入
          • 8.1.1 动画掉帧
          • 8.1.2 根因分析
          • 8.1.3 三大问题
        • 8.2 引擎原理
          • 8.2.1 插值引擎
          • 8.2.2 缓动曲线
          • 8.2.3 动画绑定协作
        • 8.3 属性动画体系
          • 8.3.1 三种基础动画
          • 8.3.2 串行与并行
          • 8.3.3 命令式控制
        • 8.4 Behavior 动画
          • 8.4.1 拦截机制
          • 8.4.2 多属性组合
        • 8.5 状态机
          • 8.5.1 声明式状态
          • 8.5.2 过渡动画
          • 8.5.3 折叠面板
        • 8.6 GPU 与 CPU
          • 8.6.1 属性路径分类
          • 8.6.2 弹性动画
          • 8.6.3 Shader 动画
        • 8.7 仪表盘动效
        • 8.8 性能与陷阱
        • 8.9 新手陷阱
        • 8.10 训练题
        • 8.11 思考题
        • 8.12 速查表
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

动画与状态机原理

# 第 8 章 动画与状态机原理

本章定位:从"能动就行"到"懂插值引擎与渲染线程驱动"。QML 的动画不是 V4 JS 的 setInterval 循环——它是 Render Thread 主导的插值引擎,独立于 GUI 线程。理解 PropertyAnimation 的 easing 曲线数学、Behavior 的自动拦截、SpringAnimation 的物理模型、State/Transition 状态机,以及在嵌入式上为什么 transform 动画帧率甩 width/height 动画几条街。

# 目录介绍

  • 8.1 案例引入
    • 8.1.1 动画掉帧
    • 8.1.2 根因分析
    • 8.1.3 三大问题
  • 8.2 引擎原理
    • 8.2.1 插值引擎
    • 8.2.2 缓动曲线
    • 8.2.3 动画绑定协作
  • 8.3 属性动画体系
    • 8.3.1 三种基础动画
    • 8.3.2 串行与并行
    • 8.3.3 命令式控制
  • 8.4 Behavior 动画
    • 8.4.1 拦截机制
    • 8.4.2 多属性组合
  • 8.5 状态机
    • 8.5.1 声明式状态
    • 8.5.2 过渡动画
    • 8.5.3 折叠面板
  • 8.6 GPU 与 CPU
    • 8.6.1 属性路径分类
    • 8.6.2 弹性动画
    • 8.6.3 Shader 动画
  • 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" ——一行。

思考题:

  1. 如果中途用户连续点击两次按钮(open → closed 切换刚启动又被切回 open),reversible: true 会怎么处理?动画会"弹回来"吗?
  2. 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 循环,会发生什么?

  1. JS 是单线程——一次 GC 卡 200ms → 动画停 200ms
  2. JS 到 C++ 的每次调用都有开销——60fps × 每帧插值 → 每秒 60 次 JS→C++ 跳转
  3. 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 思考题

  1. 动画在 Render Thread 为何不直接 setProperty? 动画在 Render Thread 计算插值后,为什么不能直接在 Render Thread 里调 QQuickItem::setProperty?非要通过 Sync 阶段传回 GUI 线程?这个设计有什么深层原因?

  2. FrameAnimation vs NumberAnimation with duration: 16:两者都能产生"每帧触发"的效果——区别在哪?什么场景下必须用 FrameAnimation?

  3. 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。

不是动画本身慢——是选错了被动画的属性。

下一篇:09. Canvas与自定义渲染 (opens new window)

上次更新: 2026/07/12, 19:39:51
模型视图架构原理
Canvas与自定义渲染

← 模型视图架构原理 Canvas与自定义渲染→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式