HarmonyOS应用开发实战:猫猫大作战-animateTo 显式触发机制、与隐式 animation 的差异、AnimateParam 动画参数、
2026/7/28 1:07:18 网站建设 项目流程

前言

前面我们用.animation({ duration: 100 })(第 16、33 篇)做了猫咪下落的平滑过渡——那是隐式动画,改position自动补间。但有种场景隐式动画做不了——点击按钮时显式触发一段动画:得分加 10 时数字弹跳、合并时新猫放大出现、连击时火焰特效闪烁。这种「主动触发」要animateTo显式动画 API——在闭包里改 state,ArkUI 自动补间。

本篇以「猫猫大作战」得分弹跳、合并放大为预演场景,把animateTo 显式触发机制与隐式 animation 的差异AnimateParam 动画参数cancel 终止动画四大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–54 篇。本篇是阶段三第五篇。

一、场景拆解:得分弹跳与合并放大

「猫猫大作战」得分更新(第 31、33 篇):

this.score = this.gameEngine.getScore(); // 改 @State,HUD 刷新

当前只是数字瞬间变——玩家想得分弹跳反馈:数字放大 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 }) // 读缩放

核心问题

  1. animateTo怎么显式触发动画?和隐式.animation有什么不同?
  2. AnimateParamduration/curve/iterations等参数怎么用?
  3. 多段动画(放大→回 1)怎么串联?
  4. 动画中途要终止怎么办?

二、animateTo 显式触发机制

2.1 基本用法

animateTo({ duration: 300, curve: Curve.EaseOut }, () => { // 闭包内改 @State,ArkUI 自动补间到新值 this.scoreScale = 1.5; });

拆解

片段含义
animateTo(param, callback)显式动画 API
{ duration: 300, curve: Curve.EaseOut }AnimateParam 动画参数
() => { this.scoreScale = 1.5; }闭包,内改 state 触发补间

机制

  1. animateTo调时,记录当前scoreScale的值(如 1)。
  2. 执行闭包,this.scoreScale = 1.5改 state。
  3. ArkUI 检测到scoreScale变化,用 300ms EaseOut 缓动从 1 补间到 1.5
  4. 依赖scoreScaleText.scale平滑过渡。

关键经验animateTo 是「显式触发补间」——在闭包里改 state,ArkUI 用指定参数补间;不调 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 得分弹跳(放大 → 回 1)

@State 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 }) // 读缩放

流程

  1. 得分变化触发handleScoreChange
  2. 第一段animateToscoreScale = 1.5,300ms EaseOut 放大。
  3. onFinish触发第二段animateTo,改scoreScale = 1,200ms 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 返回的 id,state 立即停在当前值(或手动复位)。

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 用:动画 id(cancel 用) 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.3,HUD 显示歪了 // ✅ 正确:cancel 后手动复位 cancelAnimation(this.scoreAnimId); this.scoreScale = 1; // 立即复位

八、调试技巧

  1. console.info在 onFinish:追动画完成时机,验证串联。
  2. console.info在 animateTo 闭包后:追 state 改的目标值。
  3. 动画不触发排查:检查闭包内是否改 state;检查依赖 state 的属性是否绑定(如 scale)。
  4. 动画结束后 state 错排查:检查是否 cancel 了但忘复位;检查 onFinish 是否在结束后误触发。

九、性能与最佳实践

  1. 「主动触发特效」用 animateTo,「属性自然变化」用隐式 .animation——得分弹跳用显式,猫下落用隐式。
  2. 闭包内必改 state——不改无补间,animateTo 是「补间到新值」。
  3. 回调用箭头函数保留 this——普通函数 this 丢失(第 38 篇)。
  4. 多段动画用 onFinish 串联——比 setTimeout 精准,动画结束立即触发下段。
  5. 结束 cancel 所有动画 + 复位——不停动画会继续跑,onFinish 误触发。
  6. AnimateParam 选 curve——放大 EaseOut,缩小 EaseIn,移动 EaseInOut,弹跳 Spring。
  7. iterations + playMode 做闪烁——Alternate 交替闪烁,-1 无限。

十、阶段三进度(51–55)

本篇是阶段三「交互与动画」第 5 篇:

主题核心要点
51onTouch手势三阶段 Down/Move/Up
52onHover悬停进入/离开
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.ets
  • ArkUI animateTo 显式动画官方指南
  • AnimateParam 动画参数官方文档
  • Curve 缓动曲线官方文档
  • HarmonyOS 动画性能最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询