

项目地址:https://gitcode.com/szkygc/HarmonyOs_PC-PGC/blob/main/animation
在HarmonyOS应用开发中,丰富的动画效果能够显著提升用户体验,使界面更加生动和吸引人。本项目基于Qt/QML框架,实现了8种不同类型的动画效果,展示了Qt for HarmonyOS在动画开发方面的强大能力。
本项目实现的动画效果集合具有以下特性:
动画类型 | 描述 | 技术特点 |
|---|---|---|
阴影动画 | 圆形阴影从中心向外扩散再收缩 | 多层Rectangle + SequentialAnimation |
头像闪烁 | 图片透明度循环变化 | SequentialAnimation on opacity |
淡化动画 | 全屏图片淡入淡出效果 | Window + PropertyAnimation |
移动动画 | 图片在四个固定点间循环移动 | ParallelAnimation + 位置计算 |
爆开动画 | 多个图片从中心向四周散射 | Component动态创建 + 随机位置 |
旋转动画 | 图片持续旋转 | NumberAnimation on rotation |
曲线运动 | 图片沿预设路径移动 | SequentialAnimation + ParallelAnimation |
连击动画 | 数字递增显示效果 | Row + Image组合显示 |
选择理由:
效果描述: 圆形图片周围产生阴影效果,阴影半径从0逐渐增大到最大值,然后再收缩回0,形成呼吸灯效果。
实现原理:
Item {
id: shadowContainer
property real shadowRadius: 0
// 使用Repeater创建多层阴影,实现平滑的阴影效果
Repeater {
model: 20
Rectangle {
anchors.centerIn: parent
width: parent.width + shadowContainer.shadowRadius * 2 * (index / 20.0)
height: parent.height + shadowContainer.shadowRadius * 2 * (index / 20.0)
radius: 50 * scaleFactor + shadowContainer.shadowRadius * (index / 20.0)
// 黑色阴影,透明度随半径和层级递减
color: Qt.rgba(0, 0, 0, Math.max(0, 0.15 * (1 - index / 20.0) *
(shadowContainer.shadowRadius / (50 * scaleFactor))))
visible: shadowContainer.shadowRadius > 0
z: -index - 1
}
}
// 主图片
Image {
anchors.centerIn: parent
source: "qrc:/animationTest/qrc/th-c11.png"
z: 1
}
// 阴影半径动画
SequentialAnimation on shadowRadius {
id: shadowAnimation
running: false
loops: Animation.Infinite
NumberAnimation {
from: 0
to: 50 * scaleFactor
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 50 * scaleFactor
to: 0
duration: 1000
easing.type: Easing.InOutQuad
}
}
}关键技术点:
Repeater创建20层Rectangle,每层大小和透明度不同0.15 * (1 - index / 20.0) * (shadowRadius / maxRadius),确保阴影从内到外逐渐变淡效果描述: 图片透明度在1.0和0.0之间循环变化,形成闪烁效果。
实现原理:
Image {
id: opacityEffectLabel
source: "qrc:/animationTest/qrc/th-c4.png"
SequentialAnimation on opacity {
id: opacityEffectAnimation
running: false
loops: Animation.Infinite
NumberAnimation {
from: 1.0
to: 0.0
duration: 300 // 300ms淡出
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.0
to: 1.0
duration: 300 // 300ms淡入
easing.type: Easing.InOutQuad
}
}
}关键技术点:
效果描述: 创建一个全屏窗口,显示图片并逐渐淡出,然后自动销毁。
实现原理:
function startOpacityAnimation() {
if (!contentItem) {
return
}
var fadeItem = fadeItemComponent.createObject(contentItem)
if (fadeItem) {
fadeItem.startFade()
}
}
Component {
id: fadeItemComponent
Item {
id: fadeItem
anchors.fill: parent
z: 1000
Rectangle {
anchors.fill: parent
color: "transparent"
Image {
anchors.centerIn: parent
width: 100 * root.scaleFactor
height: 100 * root.scaleFactor
source: "qrc:/animationTest/qrc/th-c1.png"
}
}
SequentialAnimation {
id: fadeAnimation
PropertyAnimation {
target: fadeItem
property: "opacity"
from: 1.0
to: 0.0
duration: 3000
}
onFinished: {
fadeItem.destroy()
}
}
function startFade() {
fadeAnimation.start()
}
}
}关键技术点:
Component.createObject()动态创建Itemdestroy()释放资源z: 1000确保在最上层显示效果描述: 图片在四个固定点之间循环移动:(247, 640) → (247, 120) → (768, 120) → (768, 640) → (247, 640)。
实现原理:
Item {
id: moveLabelContainer
x: 768 * scaleFactor
y: 640 * scaleFactor
Image {
anchors.fill: parent
source: "qrc:/animationTest/qrc/th-c1.png"
}
}
property bool moveAnimationPaused: false
property bool moveAnimationStarted: false
function toggleMoveAnimation() {
// 如果正在运行,暂停
if (moveParallelAnimation.running) {
moveParallelAnimation.pause()
moveAnimationPaused = true
return
}
// 如果已暂停,恢复
if (moveAnimationPaused) {
moveAnimationPaused = false
moveParallelAnimation.start()
return
}
// 如果动画已启动过但已完成,继续下一个移动
if (moveAnimationStarted) {
continueMoveAnimation()
return
}
// 第一次启动
moveAnimationStarted = true
var currentX = moveLabelContainer.x
var currentY = moveLabelContainer.y
moveXAnimation.from = currentX
moveXAnimation.to = 247 * scaleFactor
moveYAnimation.from = currentY
moveYAnimation.to = currentY
moveParallelAnimation.start()
}
function continueMoveAnimation() {
moveAnimationPaused = false
var currentX = moveLabelContainer.x
var currentY = moveLabelContainer.y
var target247 = 247 * scaleFactor
var target768 = 768 * scaleFactor
var target120 = 120 * scaleFactor
var target640 = 640 * scaleFactor
var tolerance = 5 * scaleFactor
var nextX = currentX
var nextY = currentY
// 根据当前位置决定下一个目标位置
if (Math.abs(currentX - target247) < tolerance && Math.abs(currentY - target640) < tolerance) {
// (247, 640) → (247, 120)
nextX = currentX
nextY = target120
} else if (Math.abs(currentX - target247) < tolerance && Math.abs(currentY - target120) < tolerance) {
// (247, 120) → (768, 120)
nextX = target768
nextY = currentY
} else if (Math.abs(currentX - target768) < tolerance && Math.abs(currentY - target120) < tolerance) {
// (768, 120) → (768, 640)
nextX = currentX
nextY = target640
} else if (Math.abs(currentX - target768) < tolerance && Math.abs(currentY - target640) < tolerance) {
// (768, 640) → (247, 640)
nextX = target247
nextY = currentY
}
moveXAnimation.from = currentX
moveXAnimation.to = nextX
moveYAnimation.from = currentY
moveYAnimation.to = nextY
moveParallelAnimation.start()
}
ParallelAnimation {
id: moveParallelAnimation
PropertyAnimation {
id: moveXAnimation
target: moveLabelContainer
property: "x"
duration: 2000
}
PropertyAnimation {
id: moveYAnimation
target: moveLabelContainer
property: "y"
duration: 2000
}
onFinished: {
if (!moveAnimationPaused) {
continueMoveAnimation()
}
}
}关键技术点:
moveAnimationPaused和 moveAnimationStarted管理动画状态onFinished中自动调用 continueMoveAnimation()实现循环效果描述: 初始图片从右下角移动到中心,然后多个图片从中心向四周随机方向散射,最后整体淡出。
实现原理:
// PopAnimation.qml
Item {
id: root
property int animationNum: 12
property bool animationRunning: false
// 初始移动元素
Image {
id: initialItem
source: "qrc:/animationTest/qrc/th-c1.png"
visible: false
ParallelAnimation {
id: moveAnimation
PropertyAnimation {
id: moveXAnim
target: initialItem
property: "x"
duration: 1000
}
PropertyAnimation {
id: moveYAnim
target: initialItem
property: "y"
duration: 1000
}
PropertyAnimation {
id: moveOpacityAnim
target: initialItem
property: "opacity"
from: 1.0
to: 0.0
duration: 1000
}
onFinished: {
root.startPopAnimation()
}
}
function startMove() {
var startX = root.width - initialItem.width
var startY = root.height - initialItem.height
var endX = (root.width - initialItem.width) / 2
var endY = (root.height - initialItem.height) / 2
initialItem.x = startX
initialItem.y = startY
initialItem.opacity = 1.0
initialItem.visible = true
moveXAnim.from = startX
moveXAnim.to = endX
moveYAnim.from = startY
moveYAnim.to = endY
moveAnimation.start()
}
}
function startPopAnimation() {
// 显示所有元素并启动散射动画
for (var i = 0; i < animationItems.length; i++) {
if (animationItems[i]) {
animationItems[i].visible = true
animationItems[i].startPopAnimation()
}
}
// 开始整体淡出动画
fadeOutAnimation.start()
}
// 整体淡出动画:0.9之前保持1.0,然后150ms内淡出
SequentialAnimation {
id: fadeOutAnimation
PauseAnimation {
duration: 1350 // 0.9 * 1500 = 1350ms
}
PropertyAnimation {
target: root
property: "opacity"
from: 1.0
to: 0.0
duration: 150 // 1500 - 1350 = 150ms
}
onFinished: {
root.opacity = 1.0
root.animationRunning = false
resetAllItems()
}
}
Component {
id: animationItemComponent
Image {
id: popItem
property int itemIndex: 0
function startPopAnimation() {
// 重置到中心位置
var centerX = (root.width - width) / 2
var centerY = (root.height - height) / 2
x = centerX
y = centerY
// 随机目标位置
var descX = root.width - width
var descY = root.height - height
var targetX = Math.floor(Math.random() * Math.max(1, descX))
var targetY = Math.floor(Math.random() * Math.max(1, descY))
// 设置动画值
popXAnimation.from = centerX
popXAnimation.to = targetX
popYAnimation.from = centerY
popYAnimation.to = targetY
// 启动动画
popXAnimation.start()
popYAnimation.start()
}
PropertyAnimation on x {
id: popXAnimation
duration: 1500
easing.type: Easing.InQuad
running: false
}
PropertyAnimation on y {
id: popYAnimation
duration: 1500
easing.type: Easing.InQuad
running: false
}
}
}
}关键技术点:
Component.createObject()创建多个散射元素Math.floor(Math.random() * Math.max(1, descX))实现随机位置生成PauseAnimation实现0.9时间点前保持不透明,然后快速淡出效果描述: 图片持续旋转,支持启动和停止控制。
实现原理:
// RotationLabel.qml
Item {
id: root
property string imageSource: ""
property bool isAnimating: false
Image {
anchors.fill: parent
source: imageSource
fillMode: Image.PreserveAspectFit
}
NumberAnimation on rotation {
id: rotationAnimation
from: 0
to: 360
duration: 2000
loops: Animation.Infinite
running: root.isAnimating
}
function toggleAnimation() {
isAnimating = !isAnimating
}
}关键技术点:
isAnimating属性控制动画运行状态效果描述: 图片沿预设的曲线路径移动,经过多个关键点。
实现原理:
Image {
id: curveLabel
x: 920 * scaleFactor
y: 500 * scaleFactor
source: "qrc:/animationTest/qrc/th-c12.png"
}
function startCurveAnimation() {
if (curveAnimation.running) return
curveLabel.x = 920 * scaleFactor
curveLabel.y = 500 * scaleFactor
curveAnimation.start()
}
SequentialAnimation {
id: curveAnimation
// 第一阶段:(920, 500) → (500, 300)
ParallelAnimation {
PropertyAnimation {
target: curveLabel
property: "x"
from: 920 * scaleFactor
to: 500 * scaleFactor
duration: 750
}
PropertyAnimation {
target: curveLabel
property: "y"
from: 500 * scaleFactor
to: 300 * scaleFactor
duration: 750
}
}
// 第二阶段:(500, 300) → (310, 270)
ParallelAnimation {
PropertyAnimation {
target: curveLabel
property: "x"
from: 500 * scaleFactor
to: 310 * scaleFactor
duration: 750
}
PropertyAnimation {
target: curveLabel
property: "y"
from: 300 * scaleFactor
to: 270 * scaleFactor
duration: 750
}
}
}关键技术点:
效果描述: 显示数字递增效果,每次点击数字加1,数字由多个图片组合显示。
实现原理:
// ComBo.qml
Item {
id: root
property int comboValue: 0
Row {
anchors.centerIn: parent
spacing: 2 * scaleFactor
Repeater {
model: comboDigits.length
Image {
width: 30 * scaleFactor
height: 40 * scaleFactor
source: "qrc:/number/qrc/number/z" + comboDigits[index] + ".png"
fillMode: Image.PreserveAspectFit
}
}
}
property var comboDigits: []
function increment() {
comboValue++
updateDigits()
}
function updateDigits() {
var digits = []
var value = comboValue
if (value === 0) {
digits.push(0)
} else {
while (value > 0) {
digits.unshift(value % 10)
value = Math.floor(value / 10)
}
}
comboDigits = digits
}
Component.onCompleted: {
updateDigits()
}
}关键技术点:
问题: 移动动画需要支持暂停、继续、循环等功能,需要精确的状态管理。
解决方案:
property bool moveAnimationPaused: false
property bool moveAnimationStarted: false
function toggleMoveAnimation() {
if (moveParallelAnimation.running) {
// 正在运行 → 暂停
moveParallelAnimation.pause()
moveAnimationPaused = true
} else if (moveAnimationPaused) {
// 已暂停 → 继续
moveAnimationPaused = false
moveParallelAnimation.start()
} else if (moveAnimationStarted) {
// 已完成 → 继续下一个移动
continueMoveAnimation()
} else {
// 第一次启动
// ...
}
}关键点:
onFinished中自动继续下一个动画问题: 浮点数计算可能导致位置判断不准确,影响动画循环。
解决方案:
var tolerance = 5 * scaleFactor
if (Math.abs(currentX - target247) < tolerance &&
Math.abs(currentY - target640) < tolerance) {
// 当前位置匹配目标位置
nextX = target247
nextY = target120
}关键点:
问题: 爆开动画和淡化动画需要动态创建和销毁组件,避免内存泄漏。
解决方案:
function startOpacityAnimation() {
var fadeItem = fadeItemComponent.createObject(contentItem)
if (fadeItem) {
fadeItem.startFade()
}
}
SequentialAnimation {
onFinished: {
fadeItem.destroy() // 动画完成后销毁
}
}关键点:
Component.createObject()动态创建destroy()释放资源问题:
组件创建时 width和 height可能为0,导致位置计算错误。
解决方案:
function startMove() {
if (root.width <= 0 || root.height <= 0) {
Qt.callLater(function() {
if (root.width > 0 && root.height > 0) {
startMove()
}
})
return
}
// 正常处理...
}关键点:
Qt.callLater延迟重试问题: 需要生成随机位置,确保动画效果的随机性和多样性。
解决方案:
var targetX = Math.floor(Math.random() * Math.max(1, descX))
var targetY = Math.floor(Math.random() * Math.max(1, descY))关键点:
Math.random()返回0-1之间的浮点数Math.floor()向下取整,得到整数位置Math.max(1, descX)防止除零错误,确保随机数范围有效scaleFactor的使用:
readonly property real scaleFactor: width > 1000 ? 2.0 : 1.0
Item {
x: 768 * scaleFactor
y: 640 * scaleFactor
width: 100 * scaleFactor
height: 100 * scaleFactor
}最佳实践:
scaleFactorreadonly property确保scaleFactor不会被修改避免不必要的重绘:
// 好的做法:只在需要时运行动画
SequentialAnimation on opacity {
running: root.visible && root.text !== ""
// ...
}
// 避免:始终运行的动画
SequentialAnimation on opacity {
running: true // 即使不可见也运行,浪费资源
// ...
}最佳实践:
loops: Animation.Infinite而非手动循环duration,避免过短或过长正确的资源清理:
Component {
id: fadeItemComponent
Item {
SequentialAnimation {
onFinished: {
// 动画完成后清理资源
fadeItem.destroy()
}
}
}
}最佳实践:
确保状态一致性:
function continueMoveAnimation() {
// 清除暂停标志
moveAnimationPaused = false
// 计算下一个位置
// ...
// 设置动画值
moveXAnimation.from = currentX
moveXAnimation.to = nextX
// ...
// 启动动画
moveParallelAnimation.start()
}最佳实践:
边界情况处理:
function startPopAnimation() {
if (!contentItem) {
return // 提前返回,避免错误
}
var popItem = popAnimationComponent.createObject(contentItem)
if (popItem) {
popItem.startAnimation()
}
// 创建失败时静默处理,不抛出异常
}最佳实践:
本项目基于Qt for HarmonyOS QML平台,成功实现了8种不同类型的动画效果:
clip属性限制绘制区域动画效果集合适用于以下场景: