HarmonyOS应用开发实战:猫猫大作战-springMotion 三参数、responsiveSpringMotion 响应式弹簧、与 EaseO

📅 2026/7/28 23:03:49
HarmonyOS应用开发实战:猫猫大作战-springMotion 三参数、responsiveSpringMotion 响应式弹簧、与 EaseO
前言前面我们用animateTo/.animation做了得分弹跳、猫咪下落——但都靠Curve.EaseOut等缓动曲线模拟没有真实的物理回弹。玩家合并猫咪时想要「弹簧」反馈新猫放大出现时超过目标尺寸再回弹像橡皮筋拽拉后振荡。HarmonyOS 的curves.springMotion弹性曲线模拟真实物理弹簧——速度、刚性、阻尼三参数自动振荡回弹。本篇以「猫猫大作战」得分弹跳改用弹簧、合并新猫弹性出现为场景把springMotion 三参数、responsiveSpringMotion 响应式弹簧、与 EaseOut 缓动差异、cancel 终止弹簧四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–58 篇。本篇是阶段三第九篇。一、场景拆解得分弹跳与合并弹性反馈回顾「猫猫大作战」得分弹跳第 55 篇用animateTo EaseOuthandleScoreChange() { animateTo({ duration: 300, curve: Curve.EaseOut }, () { this.scoreScale 1.5; // 放大到 1.5EaseOut 先快后慢 }); setTimeout(() { animateTo({ duration: 200, curve: Curve.EaseIn }, () { this.scoreScale 1; // 回 1 }); }, 300); }痛点EaseOut 是「先快后慢匀减速」无回弹——放大到 1.5 就停回 1 也停缺橡皮筋反馈。玩家想放大超过 1.5如 1.6再回弹到 1.5再震荡几次到 1。合并新猫从 0 放大到 1超过 1 到 1.1 再回 0.95 再回 1弹簧感。Spring 的解法import { curves } from kit.ArkUI; // 弹性曲线速度 0.5刚性 0.8 const springCurve curves.springMotion(0.5, 0.8); handleScoreChange() { animateTo({ duration: 800, curve: springCurve }, () { this.scoreScale 1.5; // 弹簧补间到 1.5会振荡回弹 }); } // 效果1 → 1.6 → 1.4 → 1.55 → 1.45 → ... → 1.5振荡回弹稳定关键经验Spring 弹簧曲线让动画有真实物理振荡——超过目标再回弹比 EaseOut 的匀减速更有橡皮筋反馈。二、springMotion 三参数2.1 基本用法import { curves } from kit.ArkUI; const springCurve: ICurve curves.springMotion( velocity: number, // 速度响应/振荡速率 stiffness: number, // 刚性弹簧硬度 damping?: number // 阻尼振荡衰减可选 );2.2 三参数拆解参数含义范围效果velocity速度0–1响应快慢越大越快stiffness刚性0–1弹簧硬度越大振荡越少damping阻尼0–1振荡衰减越大越快停可选默认 02.3 参数组合对照// 软弹簧低刚性振荡多 const softSpring curves.springMotion(0.3, 0.2); // 硬弹簧高刚性振荡少 const hardSpring curves.springMotion(0.8, 0.9); // 强阻尼振荡快速衰减 const dampedSpring curves.springMotion(0.5, 0.5, 0.8);参数组合效果适合(0.5, 0.3)软振荡多回弹多弹跳反馈(0.8, 0.9)硬振荡少快速稳定精确移动(0.5, 0.5, 0.8)强阻尼振荡快速衰减避免眩晕关键经验「弹跳反馈」用软弹簧低刚性「精确移动」用硬弹簧高刚性——得分弹跳软猫咪定位硬。2.4 在 animation/animateTo 用// animation 隐式用弹簧 Column() .position({ x: this.catX, y: 100 }) .animation({ duration: 800, curve: springCurve }) // animateTo 显式用弹簧 animateTo({ duration: 800, curve: springCurve }, () { this.scoreScale 1.5; });三、responsiveSpringMotion 响应式弹簧3.1 响应式 vs 普通弹簧// 普通弹簧目标变从起始重新振荡 const springCurve curves.springMotion(0.5, 0.8); // catX 从 0 弹簧到 300中途改 500从当前位置重新弹簧到 500 // 响应式弹簧目标变保留当前速度顺势改向 const responsiveCurve curves.responsiveSpringMotion( velocity: number, stiffness: number, damping?: number ); // catX 从 0 弹簧到 300中途改 500保留当前速度顺势转向 500关键差异类型目标变时适合springMotion从当前位置重新振荡弹跳反馈responsiveSpringMotion保留速度顺势改向实时跟随3.2 实时跟随场景// 拖拽时猫咪跟随手指用响应式弹簧顺滑不卡顿 Column() .position({ x: this.catX, y: this.catY }) .animation({ duration: 800, curve: responsiveCurve }) // onTouch Move 改 catX/catY响应式弹簧顺滑跟随 .onTouch((event) { if (event.type TouchType.Move) { this.catX event.touches[0].x; this.catY event.touches[0].y; } })关键经验「实时跟随」用 responsiveSpringMotion——拖拽、滑动跟随手指保留速度顺势改向不卡顿。四、与 EaseOut 缓动差异4.1 视觉对比EaseOut先快后慢匀减速 1 → 1.5线性减速到目标停 Spring 弹簧 1 → 1.6 → 1.4 → 1.55 → 1.45 → ... → 1.5振荡回弹到目标4.2 duration 的作用// EaseOutduration 决定动画总时长到达目标就停 animateTo({ duration: 300, curve: Curve.EaseOut }, () { this.scoreScale 1.5; }); // 300ms 从 1 到 1.5停 // Springduration 决定振荡时长振荡在 duration 内完成 animateTo({ duration: 800,0800, curve: springCurve }, () { this.scoreScale 1.5; }); // 800ms 内振荡 1 → 1.6 → 1.4 → ... → 1.5停关键经验Spring 的 duration 是「振荡总时长」——不是匀减速时长振荡在 duration 内完成。4.3 取舍要回弹振荡反馈 ├ 是 → Spring └ 匀减速到目标即可 └ EaseOut / EaseInOut场景推荐原因得分弹跳Spring弹跳反馈真实合并新猫放大Spring出现感弹性猫咪下落EaseOut重力下落匀加速页面滑入EaseInOut两端慢中间快自然拖拽跟随responsiveSpring顺滑不卡顿五、实战得分弹跳与合并弹性5.1 改造得分弹跳用 Spring// 来源entry/src/main/ets/pages/Index.etsSpring 改造后 import { curves, Curve, AnimationParams, 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; // animateTo 用动画 state State scoreScale: number 1; State newCatScale: number 1; State newCatId: string ; 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]; // 弹簧曲线本篇重点 private scoreSpring: ICurve curves.springMotion(0.4, 0.3); // 软弹簧得分弹跳振荡多 private mergeSpring: ICurve curves.springMotion(0.6, 0.4); // 中弹簧合并新猫弹性出现 private comboSpring: ICurve curves.springMotion(0.5, 0.5, 0.7); // 强阻尼弹簧连击闪烁振荡少 private followSpring: ICurve curves.responsiveSpringMotion(0.7, 0.6); // 响应式拖拽跟随 private scoreAnimId: number -1; private mergeAnimId: number -1; private comboAnimId: number -1; startGame() { this.clearTimers(); this.0800,0800,0800, 0800, 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.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: 800, curve: this.scoreSpring }, () { this.scoreScale 1.3; // 弹簧补间到 1.3会超过再振荡回弹 }); // 效果1 → 1.5 → 1.1 → 1.4 → 1.15 → ... → 1.3振荡回弹 } // 连击闪烁强阻尼弹簧振荡少快速稳定 handleComboFlash() { this.comboFlash true; this.comboAnimId animateTo({ duration: 600, curve: this.comboSpring }, () { this.comboFlash false; // 弹簧补间到 false会振荡回弹 }); } // 合并新猫弹性出现本篇重点 handleMerge(newCatId: string) { this.newCatId newCatId; this.newCatScale 0; this.mergeAnimId animateTo({ duration: 800, curve: this.mergeSpring }, () { this.newCatScale 1; // 弹簧补间到 1会超过 1 再振荡回弹 }); // 效果0 → 1.15 → 0.9 → 1.08 → 0.95 → ... → 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.15 : 1, y: this.comboFlash ? 1.15 : 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() { Text(CatConfig[cat.level].emoji).fontSize(CatConfig[cat.level].size * 0.5) } .width(CatConfig[cat.level].size).height(CatConfig[cat.level].size) .borderRadius(CatConfig[cat.level].size / 2) .backgroundColor(CatConfig[cat.level].color) .justifyContent(FlexAlign.Center) .shadow({ radius: 4, color: rgba(0,0,0,0.2), offsetY: 2 }) // 新合并的猫弹簧放大出现 .scale({ x: cat.id this.newCatId ? this.newCatScale : 1, y: cat.id this.newCatId ? this.newCatScale : 1 }) .position({ x: cat.x * 60 (60 - CatConfig[cat.level].size) / 2, y: cat.y * 60 (60 - CatConfig[cat.level].size) / 2 }) // 猫咪下落用 Linear重力匀速非弹簧 .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 / handleColumnClick 等略 */ }5.2 触发流程得分弹跳合并猫咪得分 10oldScore与score不同。handleScoreChange触发animateTo弹簧曲线scoreSpring。scoreScale从 1 弹簧补间到 1.31 → 1.5 → 1.1 → 1.4 → 1.15 → … → 1.3振荡回弹稳定。HUD 得分数字随之振荡放大缩小800ms 完成。合并新猫弹性出现引擎合并生成新猫调handleMerge(newCatId)。newCatScale从 0 弹簧补间到 10 → 1.15 → 0.9 → 1.08 → 0.95 → … → 1。棋盘上该猫从 0 弹性放大出现超过 1 再回弹橡皮筋反馈。连击闪烁连击 ≥ 3handleComboFlash触发。comboFlash从 true 弹簧补间到 falsetrue → false → true → false → … → false强阻尼快速稳定。连击栏文字颜色随振荡闪烁强阻尼振荡少不眩晕。六、踩坑提示6.1 Spring duration 太短振荡未完// ❌ 错误duration 300ms 太短振荡还没完就停卡在中间值 animateTo({ duration: 300, curve: this.scoreSpring }, () { this.scoreScale 1.3; }); // 可能停在 1.2振荡未完成 // ✅ 正确duration 足够长振荡完成 animateTo({ duration: 800, curve: this.scoreSpring }, () { this.scoreScale 1.3; });关键经验Spring duration 要足够振荡完成——通常 600–1000ms太短卡中间值。6.2 弹簧参数超范围// ❌ 错误velocity 或 stiffness 超范围行为未定义 const badSpring curves.springMotion(2.5, -0.3); // 超范围 // ✅ 正确参数在 0–1 范围 const goodSpring curves.springMotion(0.5, 0.8);6.3 忘取消旧弹簧动画// ❌ 错误连续得分变化没取消旧弹簧多个 animateTo 并行 handleScoreChange() { // 忠了 if (this.scoreAnimId ! -1) cancelAnimation(this.scoreAnimId); this.scoreAnimId animateTo({ duration: 800, curve: this.scoreSpring }, () { this.scoreScale 1.3; }); } // 多个弹簧并行scoreScale 振荡错乱 // ✅ 正确先取消旧再启动新 handleScoreChange() { if (this.scoreAnimId ! -1) cancelAnimation(this.scoreAnimId); this.scoreAnimId animateTo({ duration: 800, curve: this.scoreSpring }, () { this.scoreScale 1.3; }); }6.4 普通弹簧用于实时跟随// ❌ 错误拖拽跟随用普通 springMotion目标变时重新振荡卡顿 Column() .position({ x: this.catX, y: this.catY }) .animation({ duration: 800, curve: curves.springMotion(0.5, 0.8) }) // 手指移动改 catX每次都重新振荡不顺滑 // ✅ 正确实时跟随用 responsiveSpringMotion Column() .position({ x: this.catX, y: this.catY }) .animation({ duration: 800, curve: curves.responsiveSpringMotion(0.7, 0.6) }) // 保留速度顺势改向顺滑跟随6.5 用普通函数丢 this// ❌ 错误普通函数 this 不指向组件 animateTo({ duration: 800, curve: this.scoreSpring }, function () { this.scoreScale 1.3; }); // ✅ 正确箭头函数保留 this第 38 篇讲过 animateTo({ duration: 800, curve: this.scoreSpring }, () { this.scoreScale 1.3; });七、调试技巧console.info在 animateTo 闭包后追 scoreScale 目标值验证弹簧补间。振荡不完成排查检查 duration 是否足够600–1000ms检查参数是否超范围。多个弹簧并行排查检查是否忘取消旧动画检查连续触发场景。DevEco Animation Inspector查看弹簧振荡过程验证物理参数。八、性能与最佳实践「弹跳反馈」用 Spring 弹簧——得分弹跳、合并出现有真实物理振荡。springMotion 三参数——velocity 速度、stiffness 刚性、damping 阻尼可选。「实时跟随」用 responsiveSpringMotion——拖拽跟随保留速度顺势改向不卡顿。duration 足够振荡完成——通常 600–1000ms太短卡中间值。连续触发先取消旧动画——避免多个弹簧并行振荡错乱。「匀减速到目标」用 EaseOut「振荡回弹」用 Spring——按场景选。九、阶段三进度51–59本篇是阶段三「交互与动画」第 9 篇篇主题核心要点51onTouch手势三阶段52onHover悬停反馈53onKeyEvent键盘/遥控按键54bindContextMenu上下文菜单55animateTo显式动画触发56animation隐式补间57Hero共享元素跨页面58transitionif 进/出转场59本篇Spring弹簧物理振荡接下来第 60 篇会覆盖Hero Style Player 播放预定义动画。总结本篇我们从 Spring 弹性物理切入掌握了springMotion 三参数velocity/stiffness/damping、responsiveSpringMotion 响应式实时跟随、与 EaseOut 缓动差异振荡回弹 vs 匀减速、cancel 终止 duration 足够四大要点并给出了得分弹跳、合并弹性出现、连击闪烁的完整 Spring 改造代码。核心要点弹跳反馈用 Spring三参数调振荡实时跟随用 responsiveduration 足够振荡完成连续触发先取消旧。下一篇我们将拆解 Hero Style Player——播放预定义动画。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etsArkUI curves 弹性曲线官方指南springMotion API 官方文档HarmonyOS 物理动画最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md