HarmonyOS应用开发实战:猫猫大作战-预定义动画概念、AnimationPlayer 播放控制、与运行时 animateTo 的差异、Lotti

发布时间:2026/7/28 0:07:52
HarmonyOS应用开发实战:猫猫大作战-预定义动画概念、AnimationPlayer 播放控制、与运行时 animateTo 的差异、Lotti 前言前面我们用animateTo/animation/Spring做了运行时计算动画——每次都从当前值补间到目标值。但有种「预定义」场景设计师在 Ptools/设计器里做出一段完整动画关键帧序列、多属性协同、贝塞尔曲线导出为.json资源开发者播放它就行——不用复算补间。HarmonyOS 的Hero Style Player或AnimationPlayer/Lottie机制就是播放预定义动画的 API。本篇以「猫猫大作战」开场 Logo 多属性协同动画、连击火焰特效为预演场景把预定义动画概念、AnimationPlayer 播放控制、与运行时 animateTo 的差异、Lottie 矢量动画四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–59 篇。本篇是阶段三第十篇阶段三收尾。一、场景拆解开场 Logo 与连击火焰「猫猫大作战」当前开场第 32 篇是主菜单瞬现——玩家想开场 Logo 多属性协同动画0–300msLogo 从 scale 0 旋转 180° 放大到 1。300–600msLogo 上下振荡 3 次弹簧。600–1000ms副标题从 opacity 0 淡入向上滑 20px。这种多属性、多段、贝塞尔曲线的复杂动画用animateTo串联要写很多代码——设计师在 Ptools 做好后导出.json开发者播放即可// 预演开场 Logo 播放预定义动画 import { AnimationPlayer, AnimationOptions } from kit.ArkUI; private logoPlayer: AnimationPlayer | null null; aboutToAppear() { // 加载预定义动画资源 this.logoPlayer AnimationPlayer.create({ resource: resources/raw/logo_animation.json // 设导出的关键帧序列 }); } Builder MainMenuView() { Column() { Image(resources/raw/logo.png) .width(200).height(200) . onComplete(() { // 图片加载完播放动画 this.logoPlayer?.play(); }) /* ... 其他主菜单 ... */ } }核心问题预定义动画 vs 运行时 animateTo 有什么不同AnimationPlayer怎么加载资源、播放、暂停、终止多段关键帧序列怎么定义Lottie 矢量动画和位图动画的区别二、预定义动画概念2.1 什么是预定义动画预定义动画是设计师在设计器Ptools、AE、LottieFiles 等里做好的完整动画描述导出为.json资源——包含关键帧序列每个时刻的属性值。多属性协同scale、rotate、opacity、position 同时变。贝塞尔曲线精确缓动。多段串联0–300ms 放大、300–600ms 振荡、600–1000ms 淡入。开发者播放这个资源即可不用复算补间——动画内容在资源里代码只管「何时播、播几次、暂停/终止」。2.2 与运行时 animateTo 的差异维度运行时 animateTo预定义 Hero Style Player动画内容代码里写补间目标设师在资源里定义多段串联onFinish 串联多次调资源内关键帧序列多属性协同多次 animateTo 各管各资源内同时定义贝塞尔曲线用 Curve 枚举或 ICurve资源内精确贝塞尔代码量多每段都写少只播资源适合「数据驱动」补间「设计师做」复杂特效关键经验「数据驱动补间」用 animateTo「设计师做的复杂特效」用预定义 Player——得分弹跳数据驱动用 animateTo开场 Logo 设计师做用 Player。2.3 资源格式预定义动画资源常见两种格式内容来源关键帧 JSON属性×时刻的数值表Ptools/设计器导出Lottie JSON矢量图形 关键帧AE Bodymovin 插件三、AnimationPlayer 播放控制3.1 创建与加载import { AnimationPlayer, AnimationOptions } from kit.ArkUI; private logoPlayer: AnimationPlayer | null null; aboutToAppear() { this.logoPlayer AnimationPlayer.create({ resource: resources/raw/logo_animation.json, // 资源路径 container: this.logoImageRef // 绑到组件可选 }); }3.2 播放控制 API// 播放 this.logoPlayer.play(); // 暂停 this.logoPlayer.pause(); // 终止 this.logoPlayer.cancel(); // 重置到起始 this.logoPlayer.reset(); // 跳到指定时刻ms this.logoPlayer.seekTo(500); // 指定从 500ms 开始播 this.logoPlayer.playFrom(500);3.3 播放选项this.logoPlayer.play({ iterations: 1, // 播放次数-1 无限 playMode: PlayMode.Normal, // 正放/倒放/交替 duration: -1, // 速率-1 用资源内时长 onStart: () { console.info(开始播); }, onFinish: () { console.info(播完); }, onCancel: () { console.info(被终止); } });3.4 播放状态查询// 当前状态 const state this.logoPlayer.state; // Playing/Paused/Stopped // 当前时刻 const time this.logoPlayer.currentTime; // ms // 总时长 const total this.logoPlayer.duration; // ms四、多段关键帧序列4.1 资源内的关键帧设计师在资源里定义多段关键帧简化示例 JSON{ duration: 1000, keyframes: { scale: [ { time: 0, value: 0 }, { time: 300, value: 1, curve: easeOut } ], rotate: [ { time: 0, value: -180 }, { time: 300, value: 0, curve: easeOut } ], translateY: [ { time: 300, value: 0 }, { time: 400, value: -10, curve: spring }, { time: 500, value: 0, curve: spring }, { time: 600, value: -5, curve: spring }, { time: 700, value: 0 } ], opacity: [ { time: 600, value: 0 }, { time: 1000, value: 1, curve: easeInOut } ] } }拆解0–300msscale0→1 easeOut 放大rotate-180→0 easeOut 旋转。300–700mstranslateY0→-10→0→-5→0 spring 上下振荡。600–1000msopacity0→1 easeInOut 淡入。关键经验多属性协同 多段串联都在资源内定义——开发者只播资源不用 onFinish 串联多次 animateTo。4.2 开发者只管播aboutToAppear() { this.logoPlayer AnimationPlayer.create({ resource: resources/raw/logo_animation.json }); } Builder MainMenuView() { Column() { Image(resources/raw/logo.png) .width(200).height(200) .onComplete(() { // 图片加载完播放预定义动画 this.logoPlayer.play({ onFinish: () { console.info(开场动画播完显示主菜单按钮); this.showMenuButtons true; // 播完才显示按钮 } }); }) /* ... */ } }五、Lottie 矢量动画5.1 Lottie vs 位图动画维度位图动画帧序列 PNGLottie 矢量动画资源多张 PNG单个 JSON尺寸大每帧全图小只存路径缩放放大模糊矢量无限缩放清晰改色难重导出易改 JSON 颜色来源序列帧工具AE Bodymovin5.2 Lottie 加载import { Lottie } from kit.ArkUI; private fireLottie: Lottie | null null; aboutToAppear() { // 连击火焰特效 Lottie this.fireLottie Lottie.create({ resource: resources/raw/fire_combo.json, container: this.fireImageRef, loops: -1 // 无限循环 }); } // 连击 ≥ 3 触发火焰 Lottie handleComboFlash() { this.fireLottie?.play(); } // 连击结束停火焰 handleComboEnd() { this.fireLottie?.pause(); }5.3 Lottie 的优势// 矢量缩放清晰 Image().width(50).width(500) // 放大到 500 也不模糊 // 改色易改 JSON 里的 fill // fill: #FF6B6B → #3498DB重播即变蓝关键经验复杂矢量特效用 Lottie——火焰、爆炸、粒子等设计师用 AE 做的矢量动画导出 Lottie JSON 播放。六、实战开场 Logo 连击火焰6.1 准备资源假设设计师导出entry/src/main/resources/raw/logo_animation.json开场 Logo 多属性动画。entry/src/main/resources/raw/fire_combo.json连击火焰 Lottie。entry/src/main/resources/raw/logo.pngLogo 静态图。6.2 改造 Index// 来源entry/src/main/ets/pages/Index.etsHero Style Player 改造后 import { AnimationPlayer, AnimationOptions, Lottie, PlayMode } from kit.ArkUI; import { curves, Curve, animateTo, cancelAnimation } from kit.ArkUI; Entry Component struct Index { State gameState: GameState GameState.IDLE; State score: number 0; State cats: Cat[] []; State combo: ComboInfo { count: 0, multiplier: 1, lastMergeTime: 0 }; State nextCatLevel: CatLevel CatLevel.SMALL; State highScore: number 0; State gameTime: number 0; State showMenuButtons: boolean false; // 开场动画播完才显示 State comboFlashing: boolean false; // 连击火焰是否在播 private gameEngine: GameEngine new GameEngine(); private gameLoopTimer: number -1; private spawnTimer: number -1; private timeTimer: number -1; private readonly cols: number[] [0, 1, 2, 3, 4]; // Hero Style Player预定义动画本篇重点 private logoPlayer: AnimationPlayer | null null; private fireLottie: Lottie | null null; aboutToAppear() { // 加载开场 Logo 预定义动画 this.logoPlayer AnimationPlayer.create({ resource: resources/raw/logo_animation.json }); // 加载连击火焰 Lottie this.fireLottie Lottie.create({ resource: resources/raw/fire_combo.json, loops: -1 // 无限循环 }); } startGame() { this.clearTimers(); this.gameEngine.reset(); this.gameState GameState.PLAYING; this.score 0; this.cats []; this.gameTime 0; this.combo { count: 0, multiplier: 1, lastMergeTime: 0 }; this.nextCatLevel this.gameEngine.getNextCatLevel(); this.gameLoopTimer setInterval(() { if (this.gameState ! GameState.PLAYING) return; const oldComboCount this.combo.count; this.cats this.gameEngine.updateCats(); this.score this.gameEngine.getScore(); this.combo this.gameEngine.getCombo(); // 连击开始0 → ≥3触发火焰 Lottie本篇重点 if (oldComboCount 3 this.combo.count 3) { this.handleComboStart(); } // 连击结束≥3 → 0停火焰 if (oldComboCount 3 this.combo.count 0) { this.handleComboEnd(); } if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100); /* spawnTimer、timeTimer 等略 */ } // 连击开始播火焰 Lottie handleComboStart() { this.comboFlashing true; this.fireLottie?.play(); } // 连击结束停火焰 handleComboEnd() { this.comboFlashing false; this.fireLottie?.pause(); } endGame() { this.gameState GameState.GAME_OVER; // 停所有预定义动画 this.logoPlayer?.cancel(); this.fireLottie?.pause(); this.comboFlashing false; /* ... 其他结束逻辑 ... */ this.clearTimers(); } aboutToDisappear() { // 组件销毁释放预定义动画资源 this.logoPlayer?.release(); this.fireLottie?.release(); this.clearTimers(); } /* pauseGame / resumeGame / handleColumnClick / clearTimers / formatTime 等略 */ Builder MainMenuView() { Column() { Spacer() // 开场 Logo播预定义动画本篇重点 Image(resources/raw/logo.png) .width(200).height(200) .onComplete(() { // 图片加载完播放预定义动画 this.logoPlayer?.play({ iterations: 1, playMode: PlayMode.Normal, onFinish: () { console.info(开场动画播完); this.showMenuButtons true; // 播完才显示按钮 } }); }) .margin({ bottom: 40 }) Text(猫猫大作战) .fontSize(28).fontWeight(FontWeight.Bold).fontColor(#2C3E50) .margin({ bottom: 8 }) Text(合并同类挑战最高分) .fontSize(14).fontColor(#7F8C8D) .margin({ bottom: 40 }) // 播完动画才显示按钮 if (this.showMenuButtons) { Button(开始游戏) .width(200).height(56) .fontSize(18).fontWeight(FontWeight.Bold) .fontColor(#FFFFFF).backgroundColor(#2ECC71) .borderRadius(28).margin({ bottom: 16 }) .onClick(() { this.startGame(); }) Button(查看历史) .width(200).height(44) .fontSize(16).fontColor(#3498DB).backgroundColor(transparent) .border({ width: 1, color: #3498DB }).borderRadius(22) .onClick(() { /* 跳战绩页 */ }) } Spacer() } .width(100%).height(100%) .linearGradient({ direction: GradientDirection.Bottom, colors: [[#E8F4F8, 0.0], [#D6EEF5, 0.5], [#C9E8F2, 1.0]] }) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) // 主菜单转场 .transition( { type: TransitionType.All, opacity: 0, translate: { x: -300 } }, { duration: 300, curve: Curve.EaseInOut } ) } Builder GameHUD() { Row() { Column() { Text(得分).fontSize(11).fontColor(#95A5A6) Text(this.score.toString()) .fontSize(22).fontWeight(FontWeight.Bold).fontColor(#2C3E50) }.alignItems(HorizontalAlign.Start) Spacer() // 连击栏叠火焰 Lottie本篇重点 if (this.combo.count 1) { Stack() { // 火焰 Lottie 层连击 ≥ 3 时播 if (this.comboFlashing) { Image() .width(80).height(80) .bindLottie(this.fireLottie) // 绑 Lottie 播放 } // 连击文字层 Text( x${this.combo.multiplier}) .fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) } .width(80).height(80) .justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center) } Spacer() Column() { Text(时间).fontSize(11).fontColor(#95A5A6) Text(this.formatTime(this.gameTime)) .fontSize(18).fontWeight(FontWeight.Medium).fontColor(#2C3E50) }.alignItems(HorizontalAlign.End) } .width(100%).padding({ left: 20, right: 20, top: 12, bottom: 8 }) } Builder GameView() { Column() { this.GameHUD() Column() { Row() { /* 预告区 */ } Stack() { /* 棋盘背景 */ ForEach(this.cats, (cat: Cat) { /* ... 猫咪渲染 ... */ }, (cat: Cat) cat.id) Row() { ForEach(this.cols, (col: number) { Column() .width(GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .backgroundColor(rgba(0,0,0,0)) .onClick(() { this.handleColumnClick(col); }) }, (col: number) click_${col}) } } .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .borderRadius(12).clip(true).backgroundColor(#D6EEF5) }.alignItems(HorizontalAlign.Center) Spacer() Row() { /* 底部控制栏 */ } .width(100%).padding({ left: 24, right: 24, bottom: 24, top: 12 }) } .width(100%).height(100%) .linearGradient({ direction: GradientDirection.Bottom, colors: [[#E8F4F8, 0.0], [#D6EEF5, 0.5], [#C9E8F2, 1.0]] }) .alignItems(HorizontalAlign.Center) .transition( { type: TransitionType.All, opacity: 0, translate: { x: 300 } }, { duration: 300, curve: Curve.EaseInOut } ) } build() { Stack() { if (this.gameState GameState.IDLE) { this.MainMenuView() } else { this.GameView() } if (this.gameState GameState.PAUSED) { this.PauseOverlay() } if (this.gameState GameState.GAME_OVER) { this.GameOverOverlay() } } .width(100%).height(100%) } /* PauseOverlay / GameOverOverlay / StatItem 等略 */ }6.3 触发流程开场 Logo 动画应用启动aboutToAppear加载logo_animation.json创建 Player。MainMenuView 渲染Image加载logo.png。onComplete触发logoPlayer.play()。播放 0–300msLogo scale 0→1 easeOut 放大rotate -180→0 easeOut 旋转。播放 300–700msLogo translateY 上下振荡 spring。播放 600–1000ms副标题 opacity 0→1 easeInOut 淡入。onFinish触发showMenuButtons true主菜单按钮淡入显示。连击火焰 Lottie玩家连击 ≥ 3handleComboStart触发fireLottie.play()。Lottie 无限循环播放火焰矢量动画loops: -1。连击栏 Stack 叠加 Lottie 层 文字层视觉上火焰在文字下燃烧。连击结束count → 0handleComboEnd调fireLottie.pause()火焰停。七、踩坑提示7.1 忘在 aboutToDisappear 释放// ❌ 错误没释放 Player/Lottie资源泄漏 aboutToDisappear() { this.clearTimers(); // 忘了 this.logoPlayer?.release(); // 忘了 this.fireLottie?.release(); } // ✅ 正确销毁时释放 aboutToDisappear() { this.logoPlayer?.release(); this.fireLottie?.release(); this.clearTimers(); }7.2 资源路径错误// ❌ 错误资源路径错Player 加载失败 AnimationPlayer.create({ resource: wrong/path.json }); // ✅ 正确路径用 raw 下的相对路径 AnimationPlayer.create({ resource: resources/raw/logo_animation.json });7.3 播放前未加载完// ❌ 错误aboutToAppear 异步加载未完立即 play 报错 aboutToAppear() { this.logoPlayer AnimationPlayer.create({ /* ... */ }); } build() { this.logoPlayer.play(); // 可能还没加载完 } // ✅ 正确在 onComplete 或 onFinish 回调里播 Image().onComplete(() { this.logoPlayer?.play(); });7.4 忘终断导致动画继续// ❌ 错误游戏结束没停 Lottie火焰还在播 endGame() { this.gameState GameState.GAME_OVER; // 忘了 this.fireLottie?.pause(); } // ✅ 正确结束停所有 endGame() { this.fireLottie?.pause(); this.logoPlayer?.cancel(); this.comboFlashing false; }7.5 用普通函数丢 this// ❌ 错误回调普通函数 this 丢失 this.logoPlayer.play({ onFinish: function () { this.showMenuButtons true; } }); // ✅ 正确箭头函数保留 this第 38 篇讲过 this.logoPlayer.play({ onFinish: () { this.showMenuButtons true; } });八、调试技巧console.info在 onFinish追动画播完时机验证流程。console.info在 aboutToAppear追 Player/Lottie 创建成功。不播放排查检查资源路径检查是否在加载完前调 play检查 Player 是否 null。DevEco Animation Inspector查看预定义动画播放过程和关键帧。九、性能与最佳实践「设计师做的复杂特效」用预定义 Player/Lottie——开场 Logo、火焰特效等。「数据驱动补间」用 animateTo——得分弹跳、合并放大等。aboutToAppear 加载aboutToDisappear 释放——避免资源泄漏。在 onComplete/onFinish 回调里播——确保资源加载完再播。Lottie 适合矢量特效——火焰、爆炸、粒子缩放清晰改色易。结束停所有预定义动画——cancel Player、pause Lottie避免结束后继续播。回调用箭头函数保留 this——普通函数 this 丢失。十、阶段三进度与收尾51–60本篇是阶段三「交互与动画」第 10 篇阶段三收尾篇主题核心要点51onTouch手势三阶段 Down/Move/Up52onHover悬停反馈TV/PC 场景53onKeyEvent键盘/遥控按键54bindContextMenu上下文菜单55animateTo显式动画触发56animation隐式补间57Hero共享元素跨页面58transitionif 进/出转场59Spring弹簧物理振荡60本篇Hero Style Player播放预定义动画/Lottie阶段三核心收获事件四件套onTouch 手势、onHover 悬停、onKeyEvent 按键、bindContextMenu 菜单——覆盖触摸/鼠标/遥控/键盘四类输入。动画四件套animateTo 显式触发、animation 隐式补间、Hero 跨页面单元素、transition 整页面进/出——各管一场景。物理与预定义Spring 弹簧振荡、Hero Style Player/Lottie 播放设计师做的复杂特效。五种动画 API 取舍循环改 animation点击 animateToif 进出 transition跨页面 Hero设计师做 Player/Lottie。接下来阶段四61–80将进入网络与数据HTTP 请求、REST/GraphQL、数据序列化、SQLite 持久化、Preference 键值存储、文件 IO、数据绑定、列表分页、下拉刷新、上拉加载、离线缓存等。总结本篇我们从 Hero Style Player 预定义动画切入掌握了预定义 vs 运行时差异设计师做 vs 代码补间、AnimationPlayer 播放控制play/pause/cancel/seekTo、多段关键帧序列资源内定义多属性协同、**Lottie 矢量动画缩放清晰改色易**四大要点并给出了开场 Logo 连击火焰的完整改造代码。核心要点设计师做用 Player/Lottie数据驱动用 animateToaboutToAppear 加载 aboutToDisappear 释放回调用箭头函数结束停所有。下一篇我们将进入阶段四拆解 HTTP 请求——网络请求实战。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etsArkUI AnimationPlayer 预定义动画官方指南Lottie 矢量动画官方指南HarmonyOS 动画资源与最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md