鸿蒙实战:呼吸脉冲动画实现
·
鸿蒙实战:呼吸脉冲动画实现
前言(必读)

图:鸿蒙实战:呼吸脉冲动画实现(第 62 篇) 运行效果截图(HarmonyOS NEXT)
「呼吸」是最自然的生命节律。UX 设计中借用这一概念,通过有节奏的循环缩放让静态 UI 元素「活」起来——引导用户的视线、暗示可交互性、或为等待状态增添趣味。本文以项目中的三个典型案例(拍照按钮脉冲、取景框引导脉冲、墨水动画)深入讲解递归 animateTo 循环动画的实现。

图:呼吸脉冲动画——递归 animateTo 实现无限循环缩放+透明度渐变效果
一、呼吸动画的核心机制
1.1 递归 animateTo 模式
所有呼吸动画都基于同一个核心模式——在 onFinish 回调中递归调用自身:
function pulseAnimation() {
// Phase A:放大
animateTo({
duration: 1500,
onFinish: () => {
// Phase B:缩小(onFinish 中触发反方向)
animateTo({
duration: 1500,
onFinish: () => pulseAnimation() // 递归:循环回到 Phase A
}, () => {
// Phase B 的状态变化
})
}
}, () => {
// Phase A 的状态变化
})
}
1.2 呼吸动画的四要素
| 要素 | 说明 | 项目典型值 |
|---|---|---|
| 频率 | 一次完整呼吸的周期 | 3000ms(1500+1500) |
| 幅度 | 缩放或透明度的变化范围 | scale: 0.9→1.25, opacity: 0.1→0.3 |
| 曲线 | 变化速率 | Curve.Smooth(平滑起停) |
| 起始状态 | 动画循环的初始值 | 缩小状态(更符合「吸气」) |
二、拍照按钮呼吸脉冲
CapturePage 中有两套独立的呼吸脉冲动画:
2.1 中间拍照按钮的脉冲环
// 文件:pages/CapturePage.ets
@State pulseScale: number = 1.0 // 脉冲环缩放
@State pulseOpacity: number = 0.3 // 脉冲环透明度
private startPulse() {
const pulse = () => {
// Phase A:缩小变亮 → 吸引拍照注意
animateTo({
duration: 1500,
curve: Curve.Smooth,
onFinish: () => {
// Phase B:放大变淡 → 模拟退开
animateTo({
duration: 1500,
curve: Curve.Smooth,
onFinish: () => pulse() // 递归循环
}, () => {
this.pulseScale = 1.25 // 放大到 125%
this.pulseOpacity = 0.1 // 变淡到 0.1
})
}
}, () => {
this.pulseScale = 0.9 // 缩小到 90%
this.pulseOpacity = 0.3 // 变亮到 0.3
})
}
pulse() // 启动循环
}
UI 结构:
// 拍照按钮的 UI 层叠
Stack() {
// 外层:脉冲环(呼吸动画载体)
Circle()
.width(76).height(76)
.fill('rgba(255,255,255,0.2)')
.scale({ x: this.pulseScale, y: this.pulseScale })
.opacity(this.pulseOpacity)
// 中层:白色外环(68px)
Circle().width(68).height(68).fill(Color.White)
// 内层:WARM 拍照按钮(56px)
Circle().width(56).height(56).fill(AppColors.WARM)
.onClick(() => this.takePhoto())
}
视觉层级示意:
外层脉冲环(76px · 呼吸动画)
┌─────────────────────────────┐
╱ ╲
│ 中层白环(68px · 静态) │
│ ┌─────────────────────────┐ │
│ │ │ │
│ │ 内层 WARM 环 (56px) │ │
│ │ (拍照按钮) │ │
│ │ │ │
│ └─────────────────────────┘ │
╲ ╱
└─────────────────────────────┘
2.2 四角引导框脉冲
// 文件:pages/CapturePage.ets
@State cornerOpacity: number = 0.6 // 四角引导框透明度
private startCornerPulse() {
const pulse = () => {
animateTo({
duration: 1200,
curve: Curve.Smooth,
onFinish: () => {
animateTo({
duration: 1200,
curve: Curve.Smooth,
onFinish: () => pulse()
}, () => { this.cornerOpacity = 1.0 }) // 变亮
}
}, () => { this.cornerOpacity = 0.45 }) // 变暗
}
pulse()
}
2.3 两类呼吸动画的参数对比
| 参数 | 按钮脉冲环 | 四角引导框 |
|---|---|---|
| 周期 | 3000ms(1500+1500) | 2400ms(1200+1200) |
| 幅度 | scale 0.9↔1.25, opacity 0.3↔0.1 | opacity 0.6↔1.0 |
| 曲线 | Curve.Smooth | Curve.Smooth |
| 变化属性 | scale + opacity 组合 | opacity 单属性 |
| 视觉节奏 | 慢速(3s 一周期) | 中速(2.4s 一周期) |
三、AnalyzingPage 的三阶段墨水动画
墨水动画比脉冲更复杂——它由三个连续阶段构成一个完整周期:
// 文件:pages/AnalyzingPage.ets(墨水动画)
@State dropOffset: number = -70 // 墨滴垂直位置
@State dropOpacity: number = 0 // 墨滴透明度
@State spreadOpacity: number = 0 // 墨迹透明度
@State spreadScale: number = 0.5 // 墨迹缩放
private startInkAnimation() {
const drop = () => {
// 重置到初始状态
this.dropOffset = -70
this.dropOpacity = 0
this.spreadOpacity = 0
this.spreadScale = 0.5
// Phase 1:墨滴下落(1000ms)
// 墨滴从上方落到底部,缓慢加速
animateTo({
duration: 1000,
curve: Curve.EaseIn,
onFinish: () => {
// Phase 2:墨迹扩散(600ms)
// 墨滴触底后向四周扩散
animateTo({
duration: 600,
curve: Curve.EaseOut,
onFinish: () => {
// Phase 3:渐隐消散(400ms)
// 墨迹淡出后开始下一轮
animateTo({
duration: 400,
curve: Curve.EaseOut,
onFinish: () => drop() // 递归循环
}, () => {
this.spreadOpacity = 0
this.dropOpacity = 0
})
}
}, () => {
this.spreadOpacity = 0.25
this.spreadScale = 2.0
})
}
}, () => {
this.dropOffset = 0 // 落到底部
this.dropOpacity = 1 // 完全可见
})
}
setTimeout(() => drop(), 300) // 延迟 300ms 启动
}
三阶段动画时序图
Phase 1: 下落(1000ms) Phase 2: 扩散(600ms) Phase 3: 渐隐(400ms)
████████████████████████████████████████████████████████████████████████████
墨水状态:
┌────┐ ╭────╮ ······
│ ● │ 下落中 │ │ 扩散 . .
└────┘ ╰────╯ ······
dropOffset: -70→0 spreadScale: 0.5→2.0 opacity: 0.25→0
dropOpacity: 0→1 spreadOpacity: 0→0.25 全部归零
等待 300ms 后开始 完成后递归回到 Phase 1 → 无限循环
三阶段 vs 两阶段呼吸的对比
| 维度 | 两阶段呼吸(CapturePage) | 三阶段墨水动画(AnalyzingPage) |
|---|---|---|
| 阶段数 | Phase A → Phase B → 递归 | Phase 1 → Phase 2 → Phase 3 → 递归 |
| 复杂度 | 低(2 次嵌套) | 中(3 次嵌套) |
| 视觉效果 | 单属性节奏变化 | 完整的事件序列(下落→扩散→消散) |
| 动画曲线 | 全场 Smooth | 分阶段 EaseIn / EaseOut / EaseOut |
四、TapSharePage 的三层脉冲涟漪
TapSharePage 实现了 NFC 碰一碰的脉冲涟漪效果——三个圆圈依次向外扩散:
// 文件:pages/TapSharePage.ets(三圈涟漪动画)
@State ripple1Scale: number = 1.0
@State ripple1Opacity: number = 0.3
@State ripple2Scale: number = 1.0
@State ripple2Opacity: number = 0.3
@State ripple3Scale: number = 1.0
@State ripple3Opacity: number = 0.3
private startRippleAnimation() {
const animateRing = (
scaleState: number[],
opacityState: number[],
index: number
) => {
// 重置当前环
scaleState[index] = 1.0
opacityState[index] = 0.3
animateTo({
duration: 1500,
curve: Curve.Smooth,
onFinish: () => {
scaleState[index] = 1.0
opacityState[index] = 0
// 循环
animateRing(scaleState, opacityState, index)
}
}, () => {
scaleState[index] = 1.3 // 放大到 130%
opacityState[index] = 0 // 完全透明
})
}
// 三个环相位偏移 500ms 启动
setTimeout(() => animateRing(this.rippleScales, this.rippleOpacities, 0), 0)
setTimeout(() => animateRing(this.rippleScales, this.rippleOpacities, 1), 500)
setTimeout(() => animateRing(this.rippleScales, this.rippleOpacities, 2), 1000)
}
五、呼吸动画的应用场景总结
5.1 项目中的呼吸动画全景
| 页面 | 动画元素 | 周期 | 作用 |
|---|---|---|---|
| CapturePage | 拍照按钮脉冲环 | 3s | 引导用户按拍照按钮 |
| CapturePage | 四角引导框脉冲 | 2.4s | 提示用户将字迹放入取景框 |
| AnalyzingPage | 墨水下落扩散 | 2s(1+0.6+0.4) | 增加分析等待的趣味性 |
| TapSharePage | NFC 三圈涟漪 | 1.5s × 3 | 模拟 NFC 信号辐射 |
5.2 呼吸动画 vs 入场动画
呼吸动画:永久循环
▁▂▃▅▆▇▆▅▃▂▁▂▃▅▆▇▆▅▃▂▁▂▃▅▆...
持续吸引注意力直到用户操作
入场动画:一次性
▁▂▃▅▆▇━━━━━━━━━━━━━━━━━
完成即止,不干扰后续交互
| 对比维度 | 呼吸动画 | 入场动画 |
|---|---|---|
| 播放次数 | 无限循环(递归 onFinish) | 一次性 |
| 用途 | 引导注意力 / 暗示可交互 | 内容呈现 |
| 持续时间 | 页面可见的全时段 | 500-1000ms |
| 性能影响 | 持续占用 GPU | 一次性开销 |
| 停止条件 | 用户交互后移除 | 动画自然结束 |
六、性能考量
6.1 递归动画的潜在问题
// 潜在问题:页面已消失,动画仍在运行
private startPulse() {
const pulse = () => {
animateTo({ duration: 1500, onFinish: () => {
animateTo({ duration: 1500, onFinish: () => {
pulse() // 页面 pop 后可能还在运行
}})
}})
}
pulse()
}
6.2 解决方案
// 方案 1:使用标志位控制
private isActive: boolean = false
aboutToAppear() {
this.isActive = true
this.startPulse()
}
aboutToDisappear() {
this.isActive = false // 页面消失时停止
}
private startPulse() {
if (!this.isActive) return // 检查标志位
animateTo({
duration: 1500,
onFinish: () => {
if (!this.isActive) return // 再次检查
animateTo({
duration: 1500,
onFinish: () => {
if (!this.isActive) return
this.startPulse()
}
})
}
})
}
// 方案 2:简单场景使用 setTimeout 配合 clearTimeout
private pulseTimer: number = -1
aboutToDisappear() {
clearTimeout(this.pulseTimer)
}
private startPulse() {
// 使用 setTimeout 替代 onFinish 递归
animateTo({ duration: 1500 }, () => { this.scale = 0.9 })
this.pulseTimer = setTimeout(() => {
animateTo({ duration: 1500 }, () => { this.scale = 1.25 })
this.pulseTimer = setTimeout(() => this.startPulse(), 1500)
}, 1500)
}
6.3 渲染性能
| 动画类型 | 重绘区域 | GPU 负荷 | 帧率 |
|---|---|---|---|
| scale 变换 | 仅元素自身区域 | 低(GPU 合成层) | 60fps |
| opacity 变化 | 无重绘 | 低 | 60fps |
| translate 位移 | 无重绘 | 低 | 60fps |
| 同时多环动画 | 多区域 | 中 | 50-60fps |
scale和opacity是 GPU 合成层属性,不会触发布局重排,性能最优。
七、设计指南
7.1 什么时候用呼吸动画?
| 应该用 | 不应该用 |
|---|---|
| 引导用户注意核心 CTA | 多个元素同时呼吸(用争议) |
| 暗示可交互的静态元素 | 已经足够突出的按钮 |
| 等待/加载状态的装饰 | 用户正在阅读的主要文字区域 |
| 品牌/氛围氛围渲染 | 频繁更新的数据区域 |
7.2 节奏和幅度准则
呼吸频率对照表:
慢速(3-4s):优雅、沉稳、高端 → 拍照按钮
中速(2-3s):舒适、自然、呼吸感 → 引导框
快速(1-2s):活力、急切、动态 → 通知徽标
| 应用场景 | 推荐周期 | 推荐幅度 |
|---|---|---|
| 主 CTA 引导 | 2.5-3.5s | scale ±15% |
| 辅助引导 | 2-3s | opacity ±25% |
| 加载装饰 | 1.5-2.5s | 多属性组合 |
| 品牌氛围 | 4-6s | 极轻微 ±5% |
八、最佳实践模板
/**
* 标准呼吸动画工具函数
* 用法:在 aboutToAppear 中调用,aboutToDisappear 中停止
*/
class BreathingAnimation {
private isActive: boolean = false
private scale: number = 1.0
private opacity: number = 0.3
/**
* @param scaleRange 缩放范围 [小, 大] 如 [0.9, 1.25]
* @param opacityRange 透明度范围 [大, 小] 如 [0.3, 0.1]
* @param cycleMs 单期时长(总周期 = cycleMs × 2)
*/
start(
scaleRange: [number, number],
opacityRange: [number, number],
cycleMs: number
) {
this.isActive = true
const breath = () => {
if (!this.isActive) return
// 收缩
animateTo({
duration: cycleMs,
curve: Curve.Smooth,
onFinish: () => {
if (!this.isActive) return
// 扩张
animateTo({
duration: cycleMs,
curve: Curve.Smooth,
onFinish: () => breath()
}, () => {
this.scale = scaleRange[1]
this.opacity = opacityRange[0]
})
}
}, () => {
this.scale = scaleRange[0]
this.opacity = opacityRange[1]
})
}
breath()
}
stop() {
this.isActive = false
}
}
九、注意事项与常见问题
9.1 开发注意事项
说明: 以下注意事项基于 HarmonyOS NEXT 实际项目开发经验整理,建议在动手开发前仔细阅读,可有效避免常见坑点。
在正式开发前,建议按以下步骤完成环境准备与前置检查:
- 版本确认:检查 DevEco Studio 与 SDK 版本,确保满足目标 API Level 要求
- 权限声明:在
module.json5的requestPermissions字段中提前声明所有需要的系统权限 - 设备能力检查:调用前验证设备是否支持目标能力(相机、NFC、传感器等)
- 异步封装:所有耗时操作(数据库、文件 I/O、网络请求)统一使用
async/await处理 - 资源释放:在组件
aboutToDisappear()生命周期钩子中及时释放系统资源,防止内存泄漏
9.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
提示: 建议在 DevEco Studio 中开启 ArkTS Lint 静态检查,大部分编译期问题可在开发阶段发现。
总结
本文深入讲解了呼吸脉冲动画的设计与实现:
- ✅ 递归 animateTo 模式:在 onFinish 中自调用实现无限循环
- ✅ CapturePage 拍照按钮:scale + opacity 组合双层呼吸引导
- ✅ CapturePage 四角引导框:opacity 单属性中速脉冲
- ✅ AnalyzingPage 墨水动画:三阶段复合动画(下落→扩散→消散)
- ✅ TapSharePage 三圈涟漪:相位偏移的多环扩散效果
- ✅ 性能控制:标志位停止、GPU 合成层、帧率优化
- ✅ 设计指南:使用场景评判 + 节奏/幅度对照表
最后一篇将讲解全局动画常量管理,分析 AppAnimations 常量系统的设计。
📌 收藏提示:如果您觉得本系列对您有帮助,欢迎点赞 + 关注,也欢迎在评论区交流鸿蒙动画开发的问题!
相关资源:
更多推荐




所有评论(0)