📋 项目概述

在这里插入图片描述

本文档基于一个完整的 AnimationDemo 项目,详细介绍了如何在 HarmonyOS 平台上使用 Qt 实现各种动画效果。项目实现了6种不同类型的动画演示(呼吸灯、渐变效果、移动动画、旋转动画、缩放动画、文字动画),展示了 Qt Quick 动画系统在 HarmonyOS 平台上的实际应用,为开发者提供了丰富的动画效果参考。

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

仓库地址:https://atomgit.com/szkygc/HarmonyOs_PC-PGC/tree/main/Qt/AnimationDemo
环境搭建参考
Mac构建Qt鸿蒙项目:https://blog.csdn.net/qq_46169531/article/details/155111784
官方仓库:https://atomgit.com/openharmony-sig/qt
官方环境搭建参考:https://atomgit.com/openharmony-sig/qt/wiki/Home.md

✨ 主要功能

  • ✅ 6种动画效果:呼吸灯、渐变效果、移动动画、旋转动画、缩放动画、文字动画
  • ✅ 动画控制:支持开始、停止、暂停、继续等控制操作
  • ✅ 动画类型:透明度动画、位置动画、旋转动画、缩放动画、组合动画
  • ✅ ComboBox 选择器:通过下拉框切换不同的动画演示
  • ✅ 响应式布局:适配不同屏幕尺寸
  • ✅ 窗口状态处理:正确处理窗口最大化、最小化等状态变化
  • ✅ 动画生命周期管理:自动启动、停止、重置动画状态
  • ✅ 完整的触摸交互支持

🛠️ 技术栈

  • 开发框架: Qt 5.15+ for HarmonyOS
  • 编程语言: C++ / QML / JavaScript
  • 动画框架: Qt Quick Animation
  • 界面框架: Qt Quick Controls 2
  • 构建工具: CMake
  • 目标平台: HarmonyOS (OpenHarmony) / PC

🏗️ 项目架构

目录结构

AnimationDemo/
├── entry/src/main/
│   ├── cpp/
│   │   ├── main.cpp              # 应用入口(HarmonyOS适配)
│   │   ├── main.qml              # 主界面(6种动画演示)
│   │   ├── CMakeLists.txt        # 构建配置
│   │   └── qml.qrc               # QML资源文件
│   ├── module.json5              # 模块配置
│   └── resources/                # 资源文件
└── image/
    └── 演示示例.gif              # 演示动图

组件层次结构

ApplicationWindow (main.qml)
├── Column (主布局)
│   ├── Rectangle (标题栏)
│   │   └── Row
│   │       └── Text (标题)
│   ├── Rectangle (Demo选择器)
│   │   └── Row
│   │       ├── Text (标签)
│   │       └── ComboBox (动画选择器)
│   └── Rectangle (动画演示区域)
│       └── Item (动画容器)
│           ├── Rectangle (呼吸灯动画)
│           │   └── SequentialAnimation on alpha
│           ├── Rectangle (渐变效果动画)
│           │   ├── NumberAnimation (淡出)
│           │   └── NumberAnimation (淡入)
│           ├── Rectangle (移动动画)
│           │   └── SequentialAnimation on x
│           ├── Rectangle (旋转动画)
│           │   └── NumberAnimation on rotation
│           ├── Rectangle (缩放动画)
│           │   └── NumberAnimation on scaleValue
│           └── Item (文字动画)
│               └── NumberAnimation on yPosition

📝 核心功能实现

1. HarmonyOS 入口函数:qtmain()

⚠️ 关键要点:HarmonyOS 真机上必须使用 qtmain() 而不是 main()!

// ✅ 正确写法
extern "C" int qtmain(int argc, char **argv)
{
    // Qt 应用作为共享库加载,生命周期由 HarmonyOS 管理
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();  // ⚠️ 重要:必须调用 exec() 启动事件循环
}

// ❌ 错误写法(桌面应用方式)
int main(int argc, char *argv[])
{
    // 这种方式在 HarmonyOS 上会导致应用无法正常启动
}

原因说明:

  • HarmonyOS 将 Qt 应用作为共享库(.so)加载
  • 应用生命周期由 HarmonyOS 的 Ability 管理
  • qtmain() 是 HarmonyOS Qt 插件的标准入口点

2. OpenGL ES 表面格式配置

⚠️ 关键要点:必须在创建 QGuiApplication 之前配置 QSurfaceFormat!

// Step 1: 配置 OpenGL ES 表面格式(必须在创建应用之前!)
QSurfaceFormat format;

// 设置 Alpha 通道(透明度)
format.setAlphaBufferSize(8);      // 8 位 Alpha 通道

// 设置颜色通道(RGBA 32 位真彩色)
format.setRedBufferSize(8);        // 8 位红色通道
format.setGreenBufferSize(8);      // 8 位绿色通道
format.setBlueBufferSize(8);       // 8 位蓝色通道

// 设置深度和模板缓冲区
format.setDepthBufferSize(24);     // 24 位深度缓冲
format.setStencilBufferSize(8);    // 8 位模板缓冲

// 指定渲染类型为 OpenGL ES(HarmonyOS要求)
format.setRenderableType(QSurfaceFormat::OpenGLES);

// 指定 OpenGL ES 版本为 3.0(推荐)
format.setVersion(3, 0);

// ⚠️ 关键:必须在创建 QGuiApplication 之前设置默认格式!
QSurfaceFormat::setDefaultFormat(format);

// Step 2: 创建 Qt 应用实例(必须在设置格式之后)
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QGuiApplication app(argc, argv);

配置说明:

  • OpenGL ES 3.0:HarmonyOS 推荐使用 OpenGL ES 3.0
  • RGBA 8-8-8-8:32 位真彩色,支持透明度
  • 深度缓冲 24 位:用于 3D 渲染和层级管理
  • 模板缓冲 8 位:用于复杂图形效果

3. 呼吸灯动画实现

核心思想:通过 SequentialAnimation 实现透明度的循环变化,形成呼吸灯效果。

Rectangle {
    visible: currentDemoIndex === 0
    anchors.fill: parent
    color: "#1E1E1E"
  
    property int alpha: 20  // 透明度值(0-255)
  
    Column {
        anchors.centerIn: parent
        spacing: 30
      
        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: "呼吸灯动画"
            font.pixelSize: 36
            font.bold: true
            color: "#FFFFFF"
        }
      
        Rectangle {
            anchors.horizontalCenter: parent.horizontalCenter
            width: 200
            height: 200
            radius: 100
            color: Qt.rgba(0, 1, 0, parent.parent.alpha / 255.0)  // 使用 alpha 值
            border.color: "#FFFFFF"
            border.width: 2
        }
    }
  
    SequentialAnimation on alpha {
        running: currentDemoIndex === 0  // 只在选中时运行
        loops: Animation.Infinite  // 无限循环
        NumberAnimation {
            from: 20
            to: 255
            duration: 2500
            easing.type: Easing.InOutQuad
        }
        NumberAnimation {
            from: 255
            to: 20
            duration: 2500
            easing.type: Easing.InOutQuad
        }
    }
}

关键点:

  • SequentialAnimation on alpha:直接在 alpha 属性上应用顺序动画
  • 循环动画:先增大透明度(20→255),再减小透明度(255→20)
  • 缓动函数:使用 Easing.InOutQuad 实现平滑的加速和减速
  • 条件运行:使用 running: currentDemoIndex === 0 控制动画运行

4. 渐变效果动画实现

核心思想:使用独立的 NumberAnimation 实现淡入淡出效果,通过按钮控制。

Rectangle {
    id: fadeDemo
    visible: currentDemoIndex === 1
    anchors.fill: parent
    color: "#F5F5F5"
  
    property real targetOpacity: 1.0  // 目标透明度
  
    Column {
        anchors.centerIn: parent
        spacing: 30
      
        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: "渐变效果动画"
            font.pixelSize: 36
            font.bold: true
            color: "#333333"
        }
      
        Rectangle {
            id: fadeTarget
            anchors.horizontalCenter: parent.horizontalCenter
            width: 300
            height: 200
            color: "#2196F3"
            radius: 10
            opacity: fadeDemo.targetOpacity  // 绑定到目标透明度
        }
      
        Row {
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: 20
          
            Button {
                text: "淡出"
                width: 150
                height: 60
                font.pixelSize: 28
                onClicked: {
                    fadeOutAnimation.start()  // 启动淡出动画
                }
            }
          
            Button {
                text: "淡入"
                width: 150
                height: 60
                font.pixelSize: 28
                onClicked: {
                    fadeInAnimation.start()  // 启动淡入动画
                }
            }
        }
    }
  
    // 淡出动画
    NumberAnimation {
        id: fadeOutAnimation
        target: fadeDemo
        property: "targetOpacity"
        from: 1.0
        to: 0.0
        duration: 1000
    }
  
    // 淡入动画
    NumberAnimation {
        id: fadeInAnimation
        target: fadeDemo
        property: "targetOpacity"
        from: 0.0
        to: 1.0
        duration: 1000
    }
}

关键点:

  • 独立动画对象:使用 NumberAnimation 创建独立的动画对象
  • 手动控制:通过 start() 方法手动启动动画
  • 属性绑定:目标元素的 opacity 绑定到 targetOpacity 属性
  • 双向动画:淡出(1.0→0.0)和淡入(0.0→1.0)两个独立的动画

5. 移动动画实现

核心思想:使用 SequentialAnimation 实现元素在左右两个位置之间的循环移动。

Rectangle {
    id: moveDemo
    visible: currentDemoIndex === 2
    anchors.fill: parent
    color: "#F5F5F5"
  
    Text {
        anchors.top: parent.top
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.topMargin: 30
        text: "移动动画"
        font.pixelSize: 36
        font.bold: true
        color: "#333333"
    }
  
    Rectangle {
        id: movingLabel
        width: 150
        height: 60
        color: "#3498db"
        radius: 5
        x: 0
        y: (moveDemo.height - height) / 2  // 垂直居中
      
        Text {
            anchors.centerIn: parent
            text: "移动的标签"
            font.pixelSize: 24
            color: "#FFFFFF"
        }
      
        SequentialAnimation on x {
            id: moveAnimation
            running: currentDemoIndex === 2 && moveDemo.visible
            loops: Animation.Infinite
            NumberAnimation {
                from: 0
                to: moveDemo.width - movingLabel.width  // 移动到右边界
                duration: 2000
                easing.type: Easing.InOutQuad
            }
            NumberAnimation {
                from: moveDemo.width - movingLabel.width
                to: 0  // 移动回左边界
                duration: 2000
                easing.type: Easing.InOutQuad
            }
        }
    }
  
    // 当切换到移动动画时,重置位置并启动动画
    onVisibleChanged: {
        if (visible) {
            movingLabel.x = 0
            moveAnimation.restart()
        }
    }
}

关键点:

  • SequentialAnimation on x:直接在 x 属性上应用顺序动画
  • 动态计算:使用 moveDemo.width - movingLabel.width 计算右边界位置
  • 循环移动:先向右移动(0→右边界),再向左移动(右边界→0)
  • 状态重置:使用 onVisibleChanged 在显示时重置位置

6. 旋转动画实现

核心思想:使用 NumberAnimation 实现元素的持续旋转。

Rectangle {
    visible: currentDemoIndex === 3
    anchors.fill: parent
    color: "#F5F5F5"
  
    property real rotation: 0  // 旋转角度
  
    Column {
        anchors.centerIn: parent
        spacing: 30
      
        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: "旋转动画"
            font.pixelSize: 36
            font.bold: true
            color: "#333333"
        }
      
        Rectangle {
            anchors.horizontalCenter: parent.horizontalCenter
            width: 200
            height: 200
            color: "transparent"
          
            Rectangle {
                anchors.centerIn: parent
                width: 150
                height: 150
                color: "#9C27B0"
                radius: 10
                rotation: parent.parent.parent.rotation  // 绑定到旋转角度
              
                Text {
                    anchors.centerIn: parent
                    text: "旋转"
                    font.pixelSize: 28
                    color: "#FFFFFF"
                }
            }
        }
    }
  
    NumberAnimation on rotation {
        running: currentDemoIndex === 3
        loops: Animation.Infinite
        from: 0
        to: 360  // 360度旋转
        duration: 5000
        easing.type: Easing.Linear  // 线性缓动,匀速旋转
    }
}

关键点:

  • NumberAnimation on rotation:直接在 rotation 属性上应用动画
  • 360度旋转:从 0 度旋转到 360 度
  • 线性缓动:使用 Easing.Linear 实现匀速旋转
  • 属性绑定:子元素的 rotation 绑定到父元素的 rotation 属性

7. 缩放动画实现

核心思想:使用独立的 NumberAnimation 实现元素的缩放效果,支持手动控制。

Rectangle {
    id: scaleDemo
    visible: currentDemoIndex === 4
    anchors.fill: parent
    color: "#F5F5F5"
  
    property real scaleValue: 1.0  // 缩放值
  
    Column {
        anchors.centerIn: parent
        spacing: 30
      
        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: "缩放动画"
            font.pixelSize: 36
            font.bold: true
            color: "#333333"
        }
      
        Rectangle {
            id: scaleTarget
            anchors.horizontalCenter: parent.horizontalCenter
            width: 200 * scaleDemo.scaleValue  // 使用缩放值
            height: 200 * scaleDemo.scaleValue
            color: "#FF5722"
            radius: 10
          
            Text {
                anchors.centerIn: parent
                text: "缩放"
                font.pixelSize: 28
                color: "#FFFFFF"
            }
        }
      
        Button {
            anchors.horizontalCenter: parent.horizontalCenter
            text: scaleAnimation.running ? "停止动画" : "开始动画"
            width: 200
            height: 60
            font.pixelSize: 28
            onClicked: {
                if (scaleAnimation.running) {
                    scaleAnimation.stop()  // 停止动画
                } else {
                    // 根据当前缩放值决定目标值
                    if (scaleDemo.scaleValue === 1.0) {
                        scaleAnimation.from = 1.0
                        scaleAnimation.to = 2.0  // 放大到2倍
                    } else {
                        scaleAnimation.from = 2.0
                        scaleAnimation.to = 1.0  // 缩小到1倍
                    }
                    scaleAnimation.start()
                }
            }
        }
    }
  
    NumberAnimation {
        id: scaleAnimation
        target: scaleDemo
        property: "scaleValue"
        duration: 1000
        easing.type: Easing.InOutQuad
    }
  
    // 切换到缩放动画时,重置缩放值
    onVisibleChanged: {
        if (visible) {
            scaleValue = 1.0
        }
    }
}

关键点:

  • 动态缩放:使用 scaleValue 属性控制元素大小
  • 双向缩放:支持放大(1.0→2.0)和缩小(2.0→1.0)
  • 手动控制:通过按钮控制动画的开始和停止
  • 状态重置:切换时重置缩放值

8. 文字动画实现

核心思想:使用 NumberAnimation 实现文字从下往上的移动效果。

Item {
    id: textDemo
    visible: currentDemoIndex === 5
    anchors.fill: parent
  
    property real yPosition: -parent.height  // Y位置(初始在屏幕外)
  
    Rectangle {
        anchors.fill: parent
        color: "#1E1E1E"
      
        Text {
            anchors.top: parent.top
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.topMargin: 30
            text: "文字动画"
            font.pixelSize: 36
            font.bold: true
            color: "#FFFFFF"
        }
      
        Text {
            id: animatedText
            anchors.horizontalCenter: parent.horizontalCenter
            y: textDemo.height / 2 + textDemo.yPosition  // 使用 yPosition
            text: "欢迎使用Qt动画演示"
            font.pixelSize: 32
            color: "#00FF00"
        }
      
        Button {
            anchors.bottom: parent.bottom
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.bottomMargin: 50
            text: "重新播放"
            width: 200
            height: 60
            font.pixelSize: 28
            onClicked: {
                textDemo.yPosition = -textDemo.height  // 重置到屏幕外
                textAnimation.start()
            }
        }
    }
  
    NumberAnimation {
        id: textAnimation
        target: textDemo
        property: "yPosition"
        from: -textDemo.height  // 从屏幕外开始
        to: 0  // 移动到屏幕中央
        duration: 1000
        easing.type: Easing.OutInQuad
    }
  
    // 切换到文字动画时,自动播放
    onVisibleChanged: {
        if (visible) {
            yPosition = -height
            Qt.callLater(function() {
                textAnimation.start()
            })
        }
    }
}

关键点:

  • Y位置动画:使用 yPosition 属性控制文字的垂直位置
  • 屏幕外开始:初始位置设置为 -height(屏幕外)
  • 自动播放:切换到此动画时自动播放
  • 重新播放:提供按钮手动重新播放动画

9. ComboBox 动画选择器

核心思想:使用 ComboBox 实现动画演示的切换。

// Demo名称列表
property var demoNames: [
    "呼吸灯",
    "渐变效果",
    "移动动画",
    "旋转动画",
    "缩放动画",
    "文字动画"
]

// 当前选中的动画demo索引
property int currentDemoIndex: 0

ComboBox {
    id: demoComboBox
    width: 300
    height: 60
    model: demoNames
    currentIndex: 0
    font.pixelSize: 26
    onCurrentIndexChanged: {
        currentDemoIndex = currentIndex  // 更新当前索引
    }
}

关键点:

  • 数据模型:使用数组定义动画名称列表
  • 索引绑定:currentDemoIndex 绑定到 ComboBox 的 currentIndex
  • 自动切换:选择不同的动画时,自动更新 currentDemoIndex
  • 条件显示:每个动画使用 visible: currentDemoIndex === index 控制显示

10. 窗口状态处理

核心思想:正确处理窗口状态变化,确保动画在窗口状态改变后正常工作。

ApplicationWindow {
    id: root
  
    // 窗口状态变化处理(修复最大化黑屏问题)
    onVisibilityChanged: {
        if (visibility === Window.Windowed || visibility === Window.Maximized || visibility === Window.FullScreen) {
            Qt.callLater(function() {
                console.log("AnimationDemo: 窗口可见性变化,强制更新")
                // 强制更新所有可见的动画组件
                if (currentDemoIndex === 2) {
                    // 移动动画需要重新启动
                    movingLabel.x = 0
                }
            })
        }
    }
  
    onWindowStateChanged: {
        console.log("AnimationDemo: 窗口状态变化 - state:", windowState)
        // 窗口状态变化时,延迟更新以确保尺寸已更新
        Qt.callLater(function() {
            console.log("AnimationDemo: 窗口尺寸:", width, "x", height)
        })
    }
}

关键点:

  • 可见性处理:使用 onVisibilityChanged 处理窗口可见性变化
  • 状态重置:窗口状态改变时重置动画状态
  • 延迟更新:使用 Qt.callLater 确保在尺寸更新后再处理
  • 日志输出:输出日志便于调试

11. ⚠️ 关键配置:deviceTypes 必须包含 “2in1”

这是本文档最重要的发现!

在 entry/src/main/module.json5 文件中,deviceTypes 必须包含 "2in1":

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": [
      "default",
      "tablet",
      "2in1"  // ⚠️ 必须添加!否则打包会失败
    ],
    // ...
  }
}

错误信息:

hvigor ERROR: Failed :entry:default@PackageHap...
Ohos BundleTool [Error]: 10011001 Parse and check args invalid in hap mode.
Error Message: --json-path must be the config.json file or module.json file.

原因分析:

  • HarmonyOS PC 设备(如 MateBook)被识别为 "2in1" 设备类型
  • 如果 deviceTypes 中缺少 "2in1",打包工具无法正确识别配置文件路径
  • 这会导致打包失败,即使应用能在真机上运行

最佳实践:

"deviceTypes": [
  "default",   // 手机
  "tablet",    // 平板
  "2in1"       // ⚠️ PC/2合1设备(必须添加!)
]

🐛 常见问题与解决方案

问题 1:动画不运行

症状:选择动画后,动画效果没有显示。

原因:

  1. running 属性没有正确设置
  2. currentDemoIndex 没有正确更新
  3. 动画目标属性没有正确绑定

解决方案:

// ✅ 正确:使用条件运行
SequentialAnimation on alpha {
    running: currentDemoIndex === 0  // 明确指定运行条件
    loops: Animation.Infinite
    // ...
}

// ❌ 错误:没有设置运行条件
SequentialAnimation on alpha {
    loops: Animation.Infinite  // 缺少 running 属性
    // ...
}

问题 2:动画切换时状态不正确

症状:切换动画时,新动画从错误的状态开始。

原因:

  1. 没有在切换时重置动画状态
  2. onVisibleChanged 没有正确处理

解决方案:

Rectangle {
    id: moveDemo
    visible: currentDemoIndex === 2
  
    // ✅ 正确:切换时重置状态
    onVisibleChanged: {
        if (visible) {
            movingLabel.x = 0  // 重置位置
            moveAnimation.restart()  // 重新启动动画
        }
    }
}

问题 3:动画性能问题

症状:动画运行卡顿,不流畅。

原因:

  1. 动画持续时间过长
  2. 同时运行多个动画
  3. 使用了复杂的计算

解决方案:

// ✅ 正确:使用合适的持续时间
NumberAnimation {
    duration: 1000  // 1秒,性能良好
    easing.type: Easing.InOutQuad
}

// ❌ 错误:持续时间过长
NumberAnimation {
    duration: 10000  // 10秒,可能影响性能
}

问题 4:窗口最大化后动画停止

症状:窗口最大化后,动画不再运行。

原因:

  1. 窗口状态变化时没有重新启动动画
  2. 窗口尺寸变化导致动画计算错误

解决方案:

ApplicationWindow {
    id: root
  
    // ✅ 正确:处理窗口状态变化
    onVisibilityChanged: {
        if (visibility === Window.Windowed || visibility === Window.Maximized) {
            Qt.callLater(function() {
                // 重新启动动画
                if (currentDemoIndex === 2) {
                    movingLabel.x = 0
                    moveAnimation.restart()
                }
            })
        }
    }
}

问题 5:动画无法停止

症状:点击停止按钮后,动画仍在运行。

原因:

  1. 使用了 SequentialAnimation on property 语法,无法直接停止
  2. 没有正确使用独立的动画对象

解决方案:

// ✅ 正确:使用独立的动画对象
NumberAnimation {
    id: scaleAnimation
    target: scaleDemo
    property: "scaleValue"
    duration: 1000
}

Button {
    onClicked: {
        if (scaleAnimation.running) {
            scaleAnimation.stop()  // 可以停止
        } else {
            scaleAnimation.start()  // 可以启动
        }
    }
}

// ❌ 错误:使用 on property 语法,无法停止
NumberAnimation on scaleValue {
    duration: 1000
    // 无法直接停止
}

问题 6:打包失败 - json-path 错误

症状:

hvigor ERROR: Failed :entry:default@PackageHap...
Error Message: --json-path must be the config.json file or module.json file.

原因:module.json5 中的 deviceTypes 缺少 "2in1"。

解决方案:

// entry/src/main/module.json5
{
  "module": {
    "deviceTypes": [
      "default",
      "tablet",
      "2in1"  // ⚠️ 必须添加!
    ]
  }
}

💡 最佳实践

1. 动画类型选择

// 1. 属性动画(PropertyAnimation)
PropertyAnimation {
    target: myItem
    property: "x"
    from: 0
    to: 100
    duration: 1000
}

// 2. 数字动画(NumberAnimation)
NumberAnimation on opacity {
    from: 0.0
    to: 1.0
    duration: 1000
}

// 3. 顺序动画(SequentialAnimation)
SequentialAnimation {
    NumberAnimation { from: 0; to: 100; duration: 500 }
    NumberAnimation { from: 100; to: 0; duration: 500 }
}

// 4. 并行动画(ParallelAnimation)
ParallelAnimation {
    NumberAnimation { target: item; property: "x"; to: 100 }
    NumberAnimation { target: item; property: "y"; to: 100 }
}

2. 缓动函数选择

// 线性缓动(匀速)
easing.type: Easing.Linear

// 二次缓动(加速/减速)
easing.type: Easing.InOutQuad

// 弹性缓动(弹性效果)
easing.type: Easing.OutElastic

// 回弹缓动(回弹效果)
easing.type: Easing.OutBounce

3. 动画控制

// 启动动画
animation.start()

// 停止动画
animation.stop()

// 暂停动画
animation.pause()

// 恢复动画
animation.resume()

// 重新启动动画
animation.restart()

4. 条件运行

// 使用 running 属性控制运行
SequentialAnimation on alpha {
    running: currentDemoIndex === 0  // 条件运行
    loops: Animation.Infinite
    // ...
}

// 使用 visible 属性控制显示
Rectangle {
    visible: currentDemoIndex === 0
    // ...
}

5. 动画生命周期管理

Rectangle {
    id: demo
  
    // 显示时启动动画
    onVisibleChanged: {
        if (visible) {
            animation.start()
        } else {
            animation.stop()
        }
    }
  
    // 组件完成时初始化
    Component.onCompleted: {
        // 初始化代码
    }
}

6. 性能优化

// ✅ 正确:使用合适的持续时间
NumberAnimation {
    duration: 1000  // 1秒
}

// ✅ 正确:避免同时运行过多动画
// 只运行当前显示的动画

// ✅ 正确:使用硬件加速
// Qt Quick 默认使用硬件加速

📊 项目结构

AnimationDemo/
├── AppScope/
│   └── app.json5              # 应用配置
├── entry/
│   ├── build-profile.json5    # 构建配置
│   ├── src/
│   │   ├── main/
│   │   │   ├── cpp/
│   │   │   │   ├── main.cpp   # C++ 入口(qtmain)
│   │   │   │   ├── main.qml   # QML UI(6种动画演示)
│   │   │   │   ├── qml.qrc    # 资源文件
│   │   │   │   └── CMakeLists.txt
│   │   │   ├── module.json5   # ⚠️ 必须包含 "2in1"
│   │   │   └── ets/           # ArkTS 代码
│   │   └── ohosTest/
│   └── libs/                   # Qt 库文件
├── image/
│   └── 演示示例.gif           # 演示动图
└── build-profile.json5        # 根构建配置

🔧 构建配置要点

CMakeLists.txt

cmake_minimum_required(VERSION 3.5.0)
project(QtForHOSample)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

list(APPEND CMAKE_FIND_ROOT_PATH ${QT_PREFIX})
include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS 
    Concurrent Gui Network Qml Quick QuickControls2 
    Widgets QuickTemplates2 QmlWorkerScript)

add_library(entry SHARED main.cpp qml.qrc)

target_link_libraries(entry PRIVATE
    Qt${QT_VERSION_MAJOR}::Concurrent
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::Network
    Qt${QT_VERSION_MAJOR}::Qml
    Qt${QT_VERSION_MAJOR}::Quick
    Qt${QT_VERSION_MAJOR}::Widgets
    Qt${QT_VERSION_MAJOR}::QuickControls2
    Qt${QT_VERSION_MAJOR}::QuickTemplates2
    Qt${QT_VERSION_MAJOR}::QmlWorkerScript
    Qt${QT_VERSION_MAJOR}::QOpenHarmonyPlatformIntegrationPlugin  # HarmonyOS 插件
)

module.json5(关键配置)

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": [
      "default",
      "tablet",
      "2in1"  // ⚠️ 必须添加!否则打包会失败
    ],
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceType": [
      "default",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

📚 参考资源

Qt 官方文档


🎉 总结

通过本项目的开发实践,我们总结了以下关键要点:

  1. ✅ 必须使用 qtmain() 作为入口函数,而不是 main()
  2. ✅ OpenGL ES 配置必须在创建应用之前完成
  3. ✅ deviceTypes 必须包含 "2in1",否则打包会失败
  4. ✅ 使用 SequentialAnimation 实现顺序动画,适合循环效果
  5. ✅ 使用 NumberAnimation 实现属性动画,支持手动控制
  6. ✅ 使用 running 属性控制动画运行,实现条件运行
  7. ✅ 使用 onVisibleChanged 处理动画生命周期,确保状态正确
  8. ✅ 选择合适的缓动函数,提升动画效果
  9. ✅ 处理窗口状态变化,确保动画在窗口状态改变后正常工作
  10. ✅ 使用 ComboBox 实现动画切换,提升用户体验
  11. ✅ 避免同时运行过多动画,优化性能
  12. ✅ 使用合适的动画持续时间,平衡效果和性能

这些经验对于在 HarmonyOS 平台上开发 Qt 应用至关重要,特别是涉及动画效果的场景。希望本文档能帮助开发者避免常见陷阱,快速上手 Qt for HarmonyOS 开发,并创建出流畅美观的动画效果。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐