HarmonyOS应用开发实战:猫猫大作战-animateTo 显式触发机制、与隐式 animation 的差异、AnimateParam 动画参数、

📅 2026/7/28 1:07:28
HarmonyOS应用开发实战:猫猫大作战-animateTo 显式触发机制、与隐式 animation 的差异、AnimateParam 动画参数、
前言前面我们用.animation({ duration: 100 })第 16、33 篇做了猫咪下落的平滑过渡——那是隐式动画改position自动补间。但有种场景隐式动画做不了——点击按钮时显式触发一段动画得分加 10 时数字弹跳、合并时新猫放大出现、连击时火焰特效闪烁。这种「主动触发」要animateTo显式动画 API——在闭包里改 stateArkUI 自动补间。本篇以「猫猫大作战」得分弹跳、合并放大为预演场景把animateTo 显式触发机制、与隐式 animation 的差异、AnimateParam 动画参数、cancel 终止动画四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–54 篇。本篇是阶段三第五篇。一、场景拆解得分弹跳与合并放大「猫猫大作战」得分更新第 31、33 篇this.score this.gameEngine.getScore(); // 改 StateHUD 刷新当前只是数字瞬间变——玩家想得分弹跳反馈数字放大 1.5 倍 → 0.9 倍 → 1 倍300ms 完成。合并新猫出现第 121、125 篇会专讲合并当前是直接渲染新猫——玩家想新猫放大出现从 0 倍缩放 → 1 倍200ms 完成。State.animation隐式动画只能补间「属性变化」做不了「点击时主动触发一段动」。要animateTo// 预演得分弹跳 State scoreScale: number 1; // 得分缩放 handleScoreChange() { animateTo({ duration: 300, curve: Curve.EaseOut }, () { this.scoreScale 1.5; // 放大 }); setTimeout(() { animateTo({ duration: 200, curve: Curve.EaseIn }, () { this.scoreScale 1; // 回 1 }); }, 300); } // HUD Text(this.score.toString()) .scale({ x: this.scoreScale, y: this.scoreScale }) // 读缩放核心问题animateTo怎么显式触发动画和隐式.animation有什么不同AnimateParam的duration/curve/iterations等参数怎么用多段动画放大→回 1怎么串联动画中途要终止怎么办二、animateTo 显式触发机制2.1 基本用法animateTo({ duration: 300, curve: Curve.EaseOut }, () { // 闭包内改 StateArkUI 自动补间到新值 this.scoreScale 1.5; });拆解片段含义animateTo(param, callback)显式动画 API{ duration: 300, curve: Curve.EaseOut }AnimateParam 动画参数() { this.scoreScale 1.5; }闭包内改 state 触发补间机制animateTo调时记录当前scoreScale的值如 1。执行闭包this.scoreScale 1.5改 state。ArkUI 检测到scoreScale变化用 300ms EaseOut 缓动从 1 补间到 1.5。依赖scoreScale的Text.scale平滑过渡。关键经验animateTo 是「显式触发补间」——在闭包里改 stateArkUI 用指定参数补间不调 animateTo 直接改 state 是瞬间变。2.2 与隐式 .animation 的差异// 隐式.animation 装饰改 state 自动补间 Text(this.score.toString()) .scale({ x: this.scoreScale, y: this.scoreScale }) .animation({ duration: 300, curve: Curve.EaseOut }) // ← 隐式 // 之后 this.scoreScale 1.5 自动补间 // 显式animateTo 包改 state主动触发 animateTo({ duration: 300, curve: Curve.EaseOut }, () { this.scoreScale 1.5; }); // 主动调才触发不调不改2.3 对比表维度隐式.animation显式animateTo触发改 state 自动补间主动调 animateTo参数固定在.animation({...})每次调可传不同参数多段动画❌ 难参数固定✅ 串联多次调终止❌ 难✅ cancel API适合「属性变化」补间「主动触发」特效关键经验「属性自然变化」用隐式 animation「主动触发特效」用显式 animateTo——猫咪下落用隐式position 自然变得分弹跳用显式点击主动触发。三、AnimateParam 动画参数3.1 完整参数interface AnimateParam { duration: number; // 动画时长毫秒 curve?: Curve | ICurve; // 缓动曲线 delay?: number; // 延迟开始毫秒 iterations?: number; // 重复次数-1 无限 playMode?: PlayMode; // 播放模式Normal/Reverse/Alternate 等 onFinish?: () void; // 完成回调 onClose?: () void; // 关闭回调 }3.2 duration 时长animateTo({ duration: 300 }, () { this.scoreScale 1.5; }); // 300ms 完成放大时长视觉适合100ms极短瞬微反馈300ms短顺按钮反馈本项目500ms中页面转场1000ms长慢强调动画3.3 curve 缓动曲线enum Curve { Linear, // 匀速 Ease, // 先慢后快再慢 EaseIn, // 先慢后快 EaseOut, // 先快后慢 EaseInOut, // 两端慢中间快 FastOut, // 快出 SlowOut, // 慢出 ExtremeEase, // 极端缓动 Sharp, // 锐利 Rhythm, // 节奏 Smooth, // 平滑 Deceleration, // 减速 // 等更多 }常用对照Curve效果适合Linear匀速机械移动EaseOut先快后慢放大出现本项目EaseIn先慢后快缩小消失EaseInOut两端慢中间快位置移动Spring弹性弹跳反馈关键经验放大用 EaseOut先快后慢有冲击感缩小用 EaseIn移动用 EaseInOut弹跳用 Spring。3.4 iterations 重复 playMode 模式// 重复 3 次 animateTo({ duration: 300, iterations: 3 }, () { this.flash !this.flash; }); // 无限重复 animateTo({ duration: 500, iterations: -1 }, () { this.flash !this.flash; }); // 正反交替闪烁 animateTo({ duration: 300, iterations: 5, playMode: PlayMode.Alternate }, () { this.flash !this.flash; });3.5 onFinish 完成回调animateTo({ duration: 300, curve: Curve.EaseOut, onFinish: () { console.info(放大完成开始缩小); animateTo({ duration: 200, curve: Curve.EaseIn }, () { this.scoreScale 1; }); } }, () { this.scoreScale 1.5; }); // 放大 300ms → onFinish → 缩小 200ms关键经验onFinish 串联多段动画——比 setTimeout 更准动画可能因性能提前/延后。四、多段动画串联4.1 得分弹跳放大 → 回 1State scoreScale: number 1; handleScoreChange() { // 第一段放大 animateTo({ duration: 300, curve: Curve.EaseOut, onFinish: () { // 第二段回 1 animateTo({ duration: 200, curve: Curve.EaseIn }, () { this.scoreScale 1; }); } }, () { this.scoreScale 1.5; }); } // HUD Text(this.score.toString()) .scale({ x: this.scoreScale, y: this.scoreScale }) // 读缩放流程得分变化触发handleScoreChange。第一段animateTo改scoreScale 1.5300ms EaseOut 放大。onFinish触发第二段animateTo改scoreScale 1200ms EaseIn 回 1。4.2 合并新猫放大出现State newCatScale: number 0; // 新猫缩放0 隐藏1 正常 handleMerge(newCat: Cat) { // 新猫从 0 放大到 1 animateTo({ duration: 200, curve: Curve.EaseOut }, () { this.newCatScale 1; }); } // 新猫渲染 ForEach(this.cats, (cat: Cat) { Column() { /* ... */ } .scale({ x: cat.id this.newCatId ? this.newCatScale : 1, y: cat.id this.newCatId ? this.newCatScale : 1 }) }, (cat: Cat) cat.id)4.3 三段动画预警 → 攻击 → 复位handleAttack() { // 第一段预警缩小蓄力 animateTo({ duration: 200, curve: Curve.EaseIn, onFinish: () { // 第二段攻击放大冲出 animateTo({ duration: 150, curve: Curve.EaseOut, onFinish: () { // 第三段复位 animateTo({ duration: 300, curve: Curve.EaseInOut }, () { this.scale 1; }); } }, () { this.scale 1.3; }); } }, () { this.scale 0.8; }); }关键经验多段动画用 onFinish 串联——每段完成后启动下一段时序精准。五、cancel 终止动画5.1 为什么要终止游戏结束时要立刻停所有动画——得分弹跳到一半要复位合并放大到一半要停。animateTo返回一个动画 id用cancelAnimation终止。5.2 用法private scoreAnimId: number -1; handleScoreChange() { this.scoreAnimId animateTo({ duration: 300, curve: Curve.EaseOut }, () { this.scoreScale 1.5; }); } // 终止动画 cancelAnimation(this.scoreAnimId); this.scoreScale 1; // 立即复位关键经验cancelAnimation 立即终止指定动画——传入 animateTo 返回的 idstate 立即停在当前值或手动复位。5.3 结束游戏时停所有动画endGame() { this.gameState GameState.GAME_OVER; // 停所有动画 if (this.scoreAnimId ! -1) { cancelAnimation(this.scoreAnimId); this.scoreAnimId -1; } if (this.mergeAnimId ! -1) { cancelAnimation(this.mergeAnimId); this.mergeAnimId -1; } // 复位 this.scoreScale 1; this.newCatScale 1; /* ... 其他结束逻辑 ... */ this.clearTimers(); }实战经验结束游戏要停所有动画 复位——不停动画会继续跑onFinish 还会触发改 state状态错乱。六、完整预演代码得分弹跳 合并放大// 预演Index 加 animateTo 显式动画 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; // animateTo 用动画 state State scoreScale: number 1; // 得分缩放 State newCatScale: number 1; // 新猫缩放 State newCatId: string ; // 新猫 id标记哪只是新合并的 State comboFlash: 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]; // animateTo 用动画 idcancel 用 private scoreAnimId: number -1; private mergeAnimId: number -1; private comboAnimId: number -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(); // 复位动画 state this.scoreScale 1; this.newCatScale 1; this.newCatId ; this.comboFlash false; this.gameLoopTimer setInterval(() { if (this.gameState ! GameState.PLAYING) return; const oldScore this.score; this.cats this.gameEngine.updateCats(); this.score this.gameEngine.getScore(); this.combo this.gameEngine.getCombo(); // 得分变化触发弹跳本篇重点 if (this.score ! oldScore) { this.handleScoreChange(); } // 连击 ≥ 3 触发闪烁 if (this.combo.count 3 !this.comboFlash) { this.handleComboFlash(); } if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100); /* spawnTimer、timeTimer 等略 */ } // 得分弹跳本篇重点 handleScoreChange() { if (this.scoreAnimId ! -1) cancelAnimation(this.scoreAnimId); this.scoreAnimId animateTo({ duration: 300, curve: Curve.EaseOut, onFinish: () { this.scoreAnimId -1; animateTo({ duration: 200, curve: Curve.EaseIn }, () { this.scoreScale 1; }); } }, () { this.scoreScale 1.4; }); } // 连击闪烁 handleComboFlash() { this.comboFlash true; this.comboAnimId animateTo({ duration: 500, iterations: 3, playMode: PlayMode.Alternate, onFinish: () { this.comboFlash false; this.comboAnimId -1; } }, () { this.comboFlash !this.comboFlash; // 交替闪烁 }); } // 合并新猫放大出现 handleMerge(newCatId: string) { this.newCatId newCatId; this.newCatScale 0; this.mergeAnimId animateTo({ duration: 200, curve: Curve.EaseOut, onFinish: () { this.mergeAnimId -1; this.newCatId ; // 清标记 } }, () { this.newCatScale 1; }); } endGame() { this.gameState GameState.GAME_OVER; // 停所有动画本篇重点 if (this.scoreAnimId ! -1) { cancelAnimation(this.scoreAnimId); this.scoreAnimId -1; } if (this.mergeAnimId ! -1) { cancelAnimation(this.mergeAnimId); this.mergeAnimId -1; } if (this.comboAnimId ! -1) { cancelAnimation(this.comboAnimId); this.comboAnimId -1; } // 复位 this.scoreScale 1; this.newCatScale 1; this.comboFlash false; /* ... 其他结束逻辑 ... */ this.clearTimers(); } /* pauseGame / resumeGame / clearTimers / formatTime / aboutToDisappear 等略 */ Builder GameHUD() { Row() { Column() { Text(得分).fontSize(11).fontColor(#95A5A6) // 得分 Text读 scoreScale 缩放本篇重点 Text(this.score.toString()) .fontSize(22).fontWeight(FontWeight.Bold).fontColor(#2C3E50) .scale({ x: this.scoreScale, y: this.scoreScale }) // ← 读缩放 }.alignItems(HorizontalAlign.Start) Spacer() // 连击栏闪烁时变色 if (this.combo.count 1) { Row() { Text( x${this.combo.multiplier}) .fontSize(18).fontWeight(FontWeight.Bold) .fontColor(this.comboFlash ? #FF6B6B : #E74C3C) // ← 闪烁变色 } .padding({ left: 12, right: 12, top: 4, bottom: 4 }) .backgroundColor(this.comboFlash ? rgba(255, 107, 107, 0.3) : rgba(231, 76, 60, 0.1)) .borderRadius(16) .scale({ x: this.comboFlash ? 1.1 : 1, y: this.comboFlash ? 1.1 : 1 }) } 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) { Column() { /* ... emoji ... */ } .width(/* ... */).height(/* ... */) // 新合并的猫读 newCatScale 放大出现 .scale({ x: cat.id this.newCatId ? this.newCatScale : 1, y: cat.id this.newCatId ? this.newCatScale : 1 }) .position({ x: /* ... */, y: /* ... */ }) .animation({ duration: 100, curve: Curve.Linear }) // 隐式位置补间 }, (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) } /* MainMenuView / PauseOverlay / GameOverOverlay / StatItem 等略 */ }七、踩坑提示7.1 animateTo 闭包不改 state// ❌ 错误闭包里没改 state无补间 animateTo({ duration: 300 }, () { console.info(没改 state); // 没动效 }); // ✅ 正确闭包内改 state animateTo({ duration: 300 }, () { this.scoreScale 1.5; // 改 state 触发补间 });7.2 用普通函数丢 this// ❌ 错误普通函数 this 不指向组件 animateTo({ duration: 300 }, function () { this.scoreScale 1.5; }); // ✅ 正确箭头函数保留 this第 38 篇讲过 animateTo({ duration: 300 }, () { this.scoreScale 1.5; });7.3 忘 cancel 导致动画继续跑// ❌ 错误结束游戏没 cancel动画 onFinish 还触发改 state endGame() { this.gameState GameState.GAME_OVER; // 忘了 cancelAnimation(this.scoreAnimId); // scoreScale 还在补间onFinish 还会触发 } // ✅ 正确结束 cancel 所有 复位 endGame() { if (this.scoreAnimId ! -1) { cancelAnimation(this.scoreAnimId); this.scoreAnimId -1; } this.scoreScale 1; /* ... */ }7.4 多段动画用 setTimeout 而非 onFinish// ⚠️ 可以但不准setTimeout 时序可能因性能漂移 animateTo({ duration: 300 }, () { this.scoreScale 1.5; }); setTimeout(() { animateTo({ duration: 200 }, () { this.scoreScale 1; }); }, 300); // setTimeout 300ms 不一定等于动画结束 // ✅ 推荐onFinish 精准 animateTo({ duration: 300, onFinish: () { animateTo({ duration: 200 }, () { this.scoreScale 1; }); } }, () { this.scoreScale 1.5; });7.5 忘复位 cancel 后的 state// ❌ 错误cancel 了但没复位state 停在中间值 cancelAnimation(this.scoreAnimId); // scoreScale 可能停在 1.3HUD 显示歪了 // ✅ 正确cancel 后手动复位 cancelAnimation(this.scoreAnimId); this.scoreScale 1; // 立即复位八、调试技巧console.info在 onFinish追动画完成时机验证串联。console.info在 animateTo 闭包后追 state 改的目标值。动画不触发排查检查闭包内是否改 state检查依赖 state 的属性是否绑定如 scale。动画结束后 state 错排查检查是否 cancel 了但忘复位检查 onFinish 是否在结束后误触发。九、性能与最佳实践「主动触发特效」用 animateTo「属性自然变化」用隐式 .animation——得分弹跳用显式猫下落用隐式。闭包内必改 state——不改无补间animateTo 是「补间到新值」。回调用箭头函数保留 this——普通函数 this 丢失第 38 篇。多段动画用 onFinish 串联——比 setTimeout 精准动画结束立即触发下段。结束 cancel 所有动画 复位——不停动画会继续跑onFinish 误触发。AnimateParam 选 curve——放大 EaseOut缩小 EaseIn移动 EaseInOut弹跳 Spring。iterations playMode 做闪烁——Alternate 交替闪烁-1 无限。十、阶段三进度51–55本篇是阶段三「交互与动画」第 5 篇篇主题核心要点51onTouch手势三阶段 Down/Move/Up52onHover悬停进入/离开53onKeyEvent键盘/遥控按键54bindContextMenu右键/长按上下文菜单55本篇animateTo显式动画触发多段串联cancel 终止接下来阶段三还会覆盖animation 属性动画隐式、Hero 共享元素、transition 转场、spring 物理、Hero Style Player 等。总结本篇我们从 animateTo 显式动画切入掌握了显式触发机制闭包改 state 补间、与隐式 .animation 的差异主动触发 vs 自动补间、AnimateParam 参数duration/curve/iterations/playMode/onFinish、cancel 终止 复位四大要点并给出了得分弹跳 合并放大 连击闪烁的完整代码。核心要点主动触发用 animateTo闭包箭头改 state多段用 onFinish 串联结束 cancel 所有 复位。下一篇我们将拆解 animation 属性动画——隐式补间机制。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etsArkUI animateTo 显式动画官方指南AnimateParam 动画参数官方文档Curve 缓动曲线官方文档HarmonyOS 动画性能最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md