欢迎光临
我们一直在努力

HarmonyOS6 PC端实战:动画系统设计与编排——让界面活起来

文章目录

      • 动画编排的几种基本模式
      • 为什么不用animateTo的onFinish
      • 完整案例代码
      • 代码解析
      • 关于iterations参数的使用
      • PC端动画的性能考量
      • 调试动画的小技巧
      • 写在最后

动画系统编排封面图

做应用最怕什么?界面死气沉沉。用户打开一个页面,所有元素"啪"一下全出现了,毫无生气。加上动画之后,体验直接升一个档次——Logo弹进来、标题慢慢浮现、按钮从底部滑上来,整个页面就有了节奏感。

今天聊聊在HarmonyOS6 PC端怎么编排复杂动画序列。说白了就是一句话:串行、并行、延迟、循环,把这四招组合好,什么动画效果都能搞出来。

动画编排的几种基本模式

先理清楚概念。串行就是A播完播B,B播完播C,一个接一个。并行是A和B同时开始。延迟是让某个动画等一会儿再开始。循环就是反复播放。

实际项目中,一个欢迎页的动画序列通常是这样的:Logo先弹入(并行弹性动画),然后标题淡入(串行,等Logo弹完),接着按钮滑入(串行,等标题出完),最后背景色做个缓慢的渐变循环(并行,跟前面不冲突)。

ArkUI的animateTo本身就支持duration、delay、iterations、curve这些参数,组合起来就能实现大部分编排需求。再加上setTimeout做串行的时序控制,基本上就够用了。

为什么不用animateTo的onFinish

动画系统编排概念图

有朋友可能会问,animateTo不是有onFinish回调吗?在onFinish里启动下一个动画不就行了?说实话,理论上可以,但写起来代码嵌套太深了。三个动画串行就是三层回调嵌套,五个动画直接变成回调地狱。

我更喜欢用setTimeout配合不同delay来实现串行,代码是扁平的,一眼就能看出来整个动画序列的时间线。0ms开始第一个,500ms开始第二个,1000ms开始第三个……清清楚楚。

当然onFinish也有用武之地,比如循环动画需要知道每次迭代结束的时候做点什么,或者动画被中断了需要清理状态,这时候onFinish就很合适。

完整案例代码

@Entry
@Component
struct AnimationSequenceDemo {
// Logo动画
@State logoScale: number = 0.0
@State logoOpacity: number = 0.0

// 标题动画
@State titleOpacity: number = 0.0
@State titleOffsetY: number = 30

// 副标题动画
@State subtitleOpacity: number = 0.0
@State subtitleOffsetY: number = 20

// 按钮动画
@State btn1Opacity: number = 0.0
@State btn1OffsetX: number = –60
@State btn2Opacity: number = 0.0
@State btn2OffsetX: number = 60

// 背景动画
@State bgStartColor: string = '#667eea'
@State bgEndColor: string = '#764ba2'
@State bgAnimating: boolean = false

// 底部提示
@State hintOpacity: number = 0.0

// 控制标记
@State started: boolean = false

aboutToAppear(): void {
this.startAnimationSequence()
}

startAnimationSequence(): void {
this.started = true

// 第1步:Logo弹入 (0ms开始, 弹性曲线)
setTimeout(() => {
animateTo({
duration: 600,
curve: Curve.EaseOut,
iterations: 1
}, () => {
this.logoScale = 1.0
this.logoOpacity = 1.0
})
}, 100)

// 第2步:标题淡入+上移 (500ms开始)
setTimeout(() => {
animateTo({
duration: 500,
curve: Curve.EaseOut
}, () => {
this.titleOpacity = 1.0
this.titleOffsetY = 0
})
}, 600)

// 第3步:副标题淡入 (900ms开始)
setTimeout(() => {
animateTo({
duration: 400,
curve: Curve.EaseOut
}, () => {
this.subtitleOpacity = 1.0
this.subtitleOffsetY = 0
})
}, 1000)

// 第4步:按钮从两侧滑入 (1200ms开始, 并行)
setTimeout(() => {
animateTo({
duration: 500,
curve: Curve.FastOutSlowIn
}, () => {
this.btn1Opacity = 1.0
this.btn1OffsetX = 0
this.btn2Opacity = 1.0
this.btn2OffsetX = 0
})
}, 1300)

// 第5步:底部提示脉冲动画 (1800ms开始, 循环)
setTimeout(() => {
animateTo({
duration: 1500,
curve: Curve.EaseInOut,
iterations: –1,
playMode: PlayMode.AlternateReverse
}, () => {
this.hintOpacity = 1.0
})
}, 1800)

// 第6步:背景色渐变循环 (2000ms开始)
setTimeout(() => {
this.bgAnimating = true
this.animateBackground()
}, 2000)
}

animateBackground(): void {
if (!this.bgAnimating) return
animateTo({
duration: 3000,
curve: Curve.EaseInOut,
onFinish: () => {
// 交换颜色
let temp = this.bgStartColor
this.bgStartColor = this.bgEndColor
this.bgEndColor = temp
// 继续循环
this.animateBackground()
}
}, () => {
// 触发重绘
this.bgStartColor = this.bgStartColor
})
}

resetAll(): void {
this.bgAnimating = false
this.logoScale = 0.0
this.logoOpacity = 0.0
this.titleOpacity = 0.0
this.titleOffsetY = 30
this.subtitleOpacity = 0.0
this.subtitleOffsetY = 20
this.btn1Opacity = 0.0
this.btn1OffsetX = –60
this.btn2Opacity = 0.0
this.btn2OffsetX = 60
this.hintOpacity = 0.0
this.bgStartColor = '#667eea'
this.bgEndColor = '#764ba2'
this.started = false

setTimeout(() => {
this.startAnimationSequence()
}, 300)
}

build() {
Column({ space: 0 }) {
// 右上角重播按钮
Row() {
Blank()
Button('重播动画')
.fontSize(13)
.fontColor('#ffffff')
.backgroundColor('#ffffff40')
.borderRadius(20)
.height(32)
.margin({ top: 16, right: 20 })
.onClick(() => {
this.resetAll()
})
}
.width('100%')

// Logo区域
Column() {
Stack() {
Column()
.width(80)
.height(80)
.backgroundColor('#ffffff')
.borderRadius(20)
.opacity(0.2)

Text('H')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
}
}
.scale({ x: this.logoScale, y: this.logoScale })
.opacity(this.logoOpacity)
.margin({ top: 60, bottom: 24 })

// 标题
Text('Welcome to HarmonyOS')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.opacity(this.titleOpacity)
.offset({ y: this.titleOffsetY })
.margin({ bottom: 12 })

// 副标题
Text('下一代全场景分布式操作系统')
.fontSize(16)
.fontColor('#ffffffcc')
.opacity(this.subtitleOpacity)
.offset({ y: this.subtitleOffsetY })
.margin({ bottom: 60 })

// 按钮组
Row({ space: 20 }) {
Button('开始使用')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#667eea')
.backgroundColor('#ffffff')
.borderRadius(25)
.width(150)
.height(50)
.opacity(this.btn1Opacity)
.offset({ x: this.btn1OffsetX })
.shadow({ radius: 12, color: '#00000030', offsetX: 0, offsetY: 4 })

Button('了解更多')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#ffffff')
.backgroundColor('#ffffff30')
.borderRadius(25)
.width(150)
.height(50)
.opacity(this.btn2Opacity)
.offset({ x: this.btn2OffsetX })
.border({ width: 1.5, color: '#ffffff80' })
}

Blank()

// 底部提示
Text('向下滑动继续')
.fontSize(13)
.fontColor('#ffffffaa')
.opacity(this.hintOpacity)
.margin({ bottom: 40 })
}
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [[this.bgStartColor, 0.0], [this.bgEndColor, 1.0]]
})
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
}
}

运行效果如图:在这里插入图片描述

代码解析

整个动画序列的时间线设计是这样的:100ms Logo弹入,600ms标题淡入,1000ms副标题出现,1300ms按钮滑入,1800ms底部提示开始脉冲,2000ms背景开始渐变循环。每个步骤之间留了合理的间隔,不会显得拥挤。

Logo的弹入用了scale从0到1加opacity从0到1的组合。animateTo的curve用EaseOut,就是先快后慢,视觉上有一种"弹出来然后稳住"的感觉。如果用SpringCurve会更Q弹,但这里要的是干净利落的风格。

按钮组的动画比较有意思——两个按钮从两侧向中间滑入。btn1的offsetX从-60到0(从左往右),btn2从60到0(从右往左),在同一个animateTo里同时修改,就实现了并行的对称滑入效果。

背景循环动画用了递归调用的方式。animateBackground里启动一个3秒的animateTo,onFinish里交换两个颜色然后再次调用自己。这样就实现了颜色在紫色和蓝色之间来回渐变的循环效果。

关于iterations参数的使用

底部提示的脉冲动画用了iterations: -1(无限循环)配合playMode: PlayMode.AlternateReverse。这个组合的意思是:正向播完再反向播,无限循环。opacity从0到1再到0再到1……就是一直在"呼吸"的效果。

坦白讲,iterations这个参数非常适合这种简单循环动画。但如果你的循环逻辑比较复杂(比如每次循环要改变不同的属性),还是用递归调用animateTo更灵活。

PC端动画的性能考量

HarmonyOS6 PC端跑这些动画,性能上基本不用担心。PC的GPU比手机强太多,几个属性动画同时跑完全不会卡。但有个地方要注意:如果你在动画过程中频繁触发重布局(比如改width/height),可能会影响流畅度。

我的建议是:能用transform(scale、rotate、translate)解决的,就不要改layout属性。transform走的是GPU合成层,性能极好;而改width/height会触发整个布局树的重新计算,代价大得多。

PC端还有个好处是屏幕大,动画的运动距离可以更长,视觉效果更舒服。手机上按钮从侧面滑入可能就30vp够了,PC端可以做到60-80vp,配合大屏幕的视觉冲击力更强。

调试动画的小技巧

调动画最烦的就是调时间。我的经验是:先把所有动画的duration设成3秒,慢慢看每个步骤的过渡效果,确认曲线和起止状态没问题之后,再把时间缩短到正常值。

还有个办法是在每个setTimeout的回调开头加个console.log打印当前步骤,这样你能在日志里清楚看到动画序列的执行顺序和时机,排查问题时特别有用。

写在最后

动画编排这事儿,技术含量其实不高,核心就是把时间线规划好。难的是审美——什么节奏舒服、什么曲线自然、什么时长恰当,这些需要你多看优秀的App、多调多试。代码只是工具,审美才是灵魂。先把本文的几个技巧吃透,然后多去翻翻那些获奖App的动画效果,慢慢就有感觉了。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS6 PC端实战:动画系统设计与编排——让界面活起来
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址