HarmonyOS6 PC 开发实战:脉冲动画——让按钮“活“起来的心跳效果
你有没有注意过,很多PC端应用里的"录音中"按钮会一直跳动?或者"正在连接"的图标会持续闪烁放大缩小?这种效果有个通用名字叫脉冲动画(Pulse Animation)。
脉冲动画的核心目的就一个——吸引注意力。它告诉用户:“嘿,这里有东西在进行中,别忘了我。”
在HarmonyOS6 PC端开发中,脉冲动画的实现其实挺有意思的。它不像展开折叠或者Tab切换那样是一次性触发的,而是一个持续循环的过程。怎么让动画一直跑?怎么优雅地停下来?这里面有不少门道。

效果描述
我们要做的是一个圆形按钮的脉冲效果:
- 正常情况下,圆形按钮保持静止
- 点击"开始脉冲",按钮开始持续地放大+变透明,然后缩回来恢复原样,循环往复
- 点击"停止脉冲",动画优雅地停下来,按钮回到初始状态
- 还有一个"快速脉冲"按钮,做一次快速的放大缩小就停
三种模式对应了脉冲动画在实际应用中的三种典型场景:持续提示、手动控制、一次性反馈。
状态变量与定时器
先来看状态设计:
@Entry
@Component
struct PulseDemo {
@State pulseScale: number = 1
@State pulseOpacity: number = 1
@State isPulsing: boolean = false
private pulseTimer: number = -1
// ...
}
这里有两个动画属性——pulseScale 控制缩放,pulseOpacity 控制透明度。加上一个 isPulsing 布尔值作为"开关"。
pulseTimer 是个定时器ID,虽然在这个实现里我们用了递归 setTimeout 而不是 setInterval,但保留一个定时器引用是个好习惯,方便后续扩展。
脉冲的核心:递归 setTimeout 驱动循环
这是整个脉冲动画最关键的部分。我们来看"开始脉冲"按钮的点击逻辑:
Button('开始脉冲')
.onClick(() => {
if (this.isPulsing) return
this.isPulsing = true
const pulse = () => {
if (!this.isPulsing) {
this.pulseScale = 1
this.pulseOpacity = 1
return
}
// 第一阶段:放大 + 变透明
animateTo({ duration: 600, curve: Curve.EaseOut }, () => {
this.pulseScale = 1.3
this.pulseOpacity = 0.5
})
// 第二阶段:600ms后缩回 + 恢复透明度
setTimeout(() => {
animateTo({ duration: 600, curve: Curve.EaseIn }, () => {
this.pulseScale = 1
this.pulseOpacity = 1
})
// 第三阶段:如果还在脉冲状态,继续下一轮
if (this.isPulsing) setTimeout(pulse, 200)
}, 600)
}
pulse()
})
这段代码用了递归调用来实现循环。说实话,很多人第一眼看到会觉得有点绕。我来画一下执行流程。
执行流程拆解
pulse() 被调用
│
├─ 检查 isPulsing,如果为 false,恢复初始状态并退出
│
├─ animateTo:scale 1→1.3,opacity 1→0.5(600ms,EaseOut)
│ ↓ 600ms 后
│
├─ setTimeout 触发:
│ ├─ animateTo:scale 1.3→1,opacity 0.5→1(600ms,EaseIn)
│ │ ↓ 600ms 后
│ │
│ └─ 检查 isPulsing,如果还是 true,等 200ms 后再次调用 pulse()
│ ↓ 200ms 后
│ pulse() 被调用(新的一轮)
│
└─ 循环继续...
每一轮脉冲的总时长是 600ms(放大)+ 600ms(缩回)+ 200ms(间隔)= 1400ms。中间那200ms的间隔是为了让两轮脉冲之间有一个短暂的"停顿",不至于连得太紧让用户眼睛不舒服。
为什么用递归 setTimeout 而不是 setInterval?
这是个很常见的问题。坦白讲,两种方式都能实现循环,但递归 setTimeout 有几个优势:
-
更精确的时序控制。
setInterval的回调是"固定间隔"触发的,如果某次回调执行时间较长,可能导致回调堆积。递归setTimeout是上一次执行完才安排下一次,不会出现这个问题。 -
方便做条件判断。每次循环结束前都可以检查
isPulsing,优雅地决定是否继续。如果用setInterval,你需要额外管理clearInterval的时机。 -
动画阶段更灵活。一个脉冲周期里有"放大"和"缩回"两个阶段,用
setTimeout嵌套来编排这两个阶段比用setInterval清晰得多。
缓动曲线的选择
你可能注意到了,放大阶段用的是 Curve.EaseOut,缩回阶段用的是 Curve.EaseIn。这不是随便选的。
- EaseOut(快起慢收):放大过程一开始比较快,然后减速。视觉上给人一种"弹出去"的感觉,很有冲击力。
- EaseIn(慢起快收):缩回过程一开始比较慢,然后加速回去。视觉上给人一种"被吸回去"的感觉。
这个组合模拟了物理世界中弹性体的运动规律——弹出去的时候有初速度所以快,但被阻力减速;弹回来的时候被"拉力"加速。虽然是数字动画,但这种物理感会让效果看起来自然很多。
如果你两个阶段都用 EaseInOut,脉冲效果会过于"均匀",缺少那种弹性的活力感。
动画属性的绑定
脉冲效果的视觉呈现是通过 .scale() 和 .opacity() 绑定状态变量,再用 .animation() 修饰器驱动的:
Stack() {
Column()
.width(60)
.height(60)
.backgroundColor('#007DFF')
.borderRadius(30)
.scale({ x: this.pulseScale, y: this.pulseScale })
.opacity(this.pulseOpacity)
.animation({ duration: 600, curve: Curve.EaseInOut })
Text('P')
.fontSize(22)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(80)
.height(80)
这里用了 Stack 把圆形背景和文字"P"叠加在一起。注意 .scale()、.opacity() 和 .animation() 都加在了背景的 Column 上,而 Text 不受影响——文字"P"始终保持静止。
为什么要这样设计?因为脉冲动画的目的是让背景圆跳动来吸引注意力,而文字如果也跟着放大缩小,会变得难以阅读。在PC端这种信息密度高的场景下,可读性永远排在第一位。
停止脉冲:优雅退出
停止逻辑非常简单:
Button('停止脉冲')
.onClick(() => {
this.isPulsing = false
})
就是把 isPulsing 设为 false。
然后在 pulse 函数开头有这个检查:
if (!this.isPulsing) {
this.pulseScale = 1
this.pulseOpacity = 1
return
}
当 isPulsing 变为 false 后,当前正在执行的动画会自然跑完(因为 animateTo 已经启动了),然后在下一轮 pulse 调用时,检测到 isPulsing 为 false,把属性恢复到初始值,退出循环。
这个设计的好处是停止过程是"优雅"的——不会突然跳到初始状态,而是让当前的缩放/透明度变化自然完成,再在下一个周期退出。用户不会感到突兀。
快速脉冲:一次性反馈效果
"快速脉冲"按钮实现的是另一种模式——不循环,只做一次快速的放大缩小:
Button('快速脉冲')
.onClick(() => {
this.isPulsing = false // 先停止可能正在进行的持续脉冲
animateTo({ duration: 200, curve: Curve.EaseOut }, () => {
this.pulseScale = 1.4
this.pulseOpacity = 0.4
})
setTimeout(() => {
animateTo({ duration: 200 }, () => {
this.pulseScale = 1
this.pulseOpacity = 1
})
}, 200)
})
跟持续脉冲相比,快速脉冲有几个区别:
- 时长更短:200ms vs 600ms,节奏快得多
- 幅度更大:放大到1.4倍 vs 1.3倍,透明度降到0.4 vs 0.5
- 不循环:做完一次就停
这种效果在PC端的实际应用场景很多:
- 按钮点击后的反馈动效
- 下载完成时的提示
- 发送消息成功后的确认
- 表单提交后的处理状态提示
它比单纯的静态反馈更有"确认感",但又不会像持续脉冲那样一直抢注意力。
完整代码
@Entry
@Component
struct PulseDemo {
@State pulseScale: number = 1
@State pulseOpacity: number = 1
@State isPulsing: boolean = false
private pulseTimer: number = -1
build() {
Column() {
Scroll() {
Column() {
Text('脉冲动画')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
Column() {
// 脉冲目标
Row() {
Stack() {
Column()
.width(60)
.height(60)
.backgroundColor('#007DFF')
.borderRadius(30)
.scale({ x: this.pulseScale, y: this.pulseScale })
.opacity(this.pulseOpacity)
.animation({ duration: 600, curve: Curve.EaseInOut })
Text('P')
.fontSize(22)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width(80)
.height(80)
}
.width('100%')
.height(120)
.justifyContent(FlexAlign.Center)
// 控制按钮
Row({ space: 10 }) {
Button('开始脉冲')
.onClick(() => {
if (this.isPulsing) return
this.isPulsing = true
const pulse = () => {
if (!this.isPulsing) {
this.pulseScale = 1
this.pulseOpacity = 1
return
}
animateTo({ duration: 600, curve: Curve.EaseOut }, () => {
this.pulseScale = 1.3
this.pulseOpacity = 0.5
})
setTimeout(() => {
animateTo({ duration: 600, curve: Curve.EaseIn }, () => {
this.pulseScale = 1
this.pulseOpacity = 1
})
if (this.isPulsing) setTimeout(pulse, 200)
}, 600)
}
pulse()
})
Button('停止脉冲')
.onClick(() => {
this.isPulsing = false
})
Button('快速脉冲')
.onClick(() => {
this.isPulsing = false
animateTo({ duration: 200, curve: Curve.EaseOut }, () => {
this.pulseScale = 1.4
this.pulseOpacity = 0.4
})
setTimeout(() => {
animateTo({ duration: 200 }, () => {
this.pulseScale = 1
this.pulseOpacity = 1
})
}, 200)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.margin({ top: 16 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
}
.width('100%')
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F6FA')
.padding(16)
}
}

循环动画的通用模式


脉冲动画本质上是一种"循环动画"。在ArkUI中实现循环动画,有几种常见的模式。
模式一:递归 setTimeout(本例使用的方式)
最灵活的方式。可以精确控制每个阶段的时长、缓动曲线和结束条件。适合阶段数量少、每个阶段逻辑不同的循环动画。
模式二:animateTo + onComplete 回调
animateTo 其实支持 onComplete 回调,可以在动画结束后执行后续逻辑:
const doPulse = () => {
if (!this.isPulsing) return
animateTo({
duration: 600,
curve: Curve.EaseOut,
onFinish: () => {
animateTo({
duration: 600,
curve: Curve.EaseIn,
onFinish: () => {
if (this.isPulsing) {
setTimeout(doPulse, 200)
}
}
}, () => {
this.pulseScale = 1
this.pulseOpacity = 1
})
}
}, () => {
this.pulseScale = 1.3
this.pulseOpacity = 0.5
})
}
用 onFinish 回调替代 setTimeout,时序上更精确——动画确确实实跑完了才触发下一步。代码可读性稍差一些,但在对时序精度要求高的场景下更可靠。
模式三:.repeat() 属性动画
如果你只需要简单的"来回循环"效果,.animation() 修饰器其实支持 .repeat() 配置:
Column()
.scale({ x: this.pulseScale, y: this.pulseScale })
.animation({
duration: 600,
curve: Curve.EaseInOut,
iterations: -1, // -1 表示无限循环
playMode: PlayMode.AlternateReverse // 来回交替
})
这种方式最简洁,但灵活性最低。它适合那些"从A到B来回切换"的简单循环动画,不适合需要分阶段控制的复杂循环。
脉冲动画在PC端的实际应用场景
PC端的脉冲动画用得比手机端多,原因很简单——PC屏幕大,用户注意力更容易分散,需要用动画把注意力引导到正确的位置。
录音/录屏状态指示。HarmonyOS6 PC端如果有录音或录屏功能,录制过程中按钮持续脉冲是最常见的做法。用户一眼就知道"正在录"。
下载/上传进度指示。文件传输过程中,图标做脉冲动画,暗示"正在处理"。比单纯的进度条更有存在感。
实时通讯状态。“正在输入…”、"正在连接…"这些状态配合脉冲动画,能让用户感受到"对面有人"的实时感。
紧急通知提醒。高优先级的通知图标做脉冲跳动,确保用户不会忽略。在PC端多任务环境下特别有用。
性能注意事项
脉冲动画是持续运行的,对性能有一定消耗。在PC端通常不是问题,但还是要注意几点:
-
不要同时跑太多脉冲动画。如果一个页面上有五六个元素都在脉冲跳动,CPU和GPU的负载会明显上升。建议同一时间最多两三个。
-
页面不可见时停止脉冲。如果用户切换到了别的页面,看不到的脉冲动画就是在白白消耗性能。可以在
aboutToDisappear生命周期里停止脉冲。 -
脉冲参数不要太激进。放大到1.3倍已经足够显眼了,别放到2倍3倍。过大的缩放幅度在动画过程中会导致大量的重绘计算。
小结
脉冲动画是让UI元素"活"起来的有效手段。在HarmonyOS6 PC端开发中,它的实现核心是:
- 用
animateTo驱动属性变化 - 用递归
setTimeout或onFinish回调实现循环 - 用布尔状态变量控制启停
三种脉冲模式——持续脉冲、可停脉冲、快速脉冲——覆盖了绝大部分实际应用场景。选择哪种取决于你的具体需求:是需要持续提醒,还是需要用户主动控制,还是只需要一次性反馈。
记住一个设计原则:脉冲动画是配角,不是主角。它的作用是辅助引导注意力,而不是喧宾夺主。克制使用,效果最好。
更多推荐

所有评论(0)