
前言上一篇我们用 Hero 做了「单个元素跨页面」的连续动画——小猫图飞到大猫图。但整个页面的出现/消失怎么动路由跳到新页面时新页面从右侧滑入、当前页面淡出返回时反向。这种「整页面进/出」的转场动画用transition装饰器配合if条件渲染或路由切换触发。本篇以「猫猫大作战」主菜单→游戏页、暂停弹窗进/出为场景把transition 装饰绑定、TransitionType 推/拉/全部、与 if 条件渲染协同、与 Hero 的差异四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–57 篇。本篇是阶段三第八篇。一、场景拆解主菜单与暂停弹窗的转场回顾「猫猫大作战」状态切换第 31、39 篇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() // 结束弹窗 } } }痛点if条件渲染的组件出现/消失是瞬间的——主菜单切游戏页瞬切、暂停弹窗瞬现/瞬隐。玩家感觉「弹窗突兀」缺过渡。transition 的解法Builder PauseOverlay() { Column() { /* ... 弹窗内容 ... */ } .transition({ type: TransitionType.All, opacity: 0, scale: 0.8 }, { duration: 300, curve: Curve.EaseInOut }) } // 出现opacity 0→1, scale 0.8→1300ms 淡入放大 // 消失opacity 1→0, scale 1→0.8300ms 淡出缩小关键经验transition 让if出现/消失的组件有进/出动画——不再瞬切过渡平滑。二、transition 装饰绑定2.1 基本用法Column() { /* ... */ } .transition({ type: TransitionType.All, opacity: 0 }, { duration: 300, curve: Curve.EaseInOut })拆解片段含义.transition(translateParam, animationParam)转场装饰器{ type: TransitionType.All, opacity: 0 }TranslateParam转场类型起始属性{ duration: 300, curve: Curve.EaseInOut }AnimationParam时长曲线机制组件出现if条件变 true从opacity: 0补间到当前 opacity1300ms 淡入。组件消失if条件变 false从当前 opacity1补间到opacity: 0300ms 淡出完成后销毁。2.2 TranslateParam 转场参数interface TranslateParam { type?: TransitionType; // 转场类型Push/Pop/All opacity?: number; // 透明度起始值 scale?: number; // 缩放起始值或 {x, y} 分轴 translate?: TranslateParam; // 平移起始值{x, y} rotate?: number; // 旋转起始值度 }2.3 AnimationParam 动画参数{ duration: 300, // 转场时长 curve: Curve.EaseInOut, // 缓动曲线 delay: 0 // 延迟 }三、TransitionType 推/拉/全部3.1 三种类型enum TransitionType { Push, // 进组件出现时动 Pop, // 出组件消失时动 All // 全出现和消失都动 }3.2 场景对照场景type效果暂停弹窗进/出都动All淡入放大 淡出缩小新页面只管进Push滑入消失瞬切旧页面只管出Pop滑出出现瞬切关键经验多数弹窗用All——进/出都动才协调只在「单向场景」用 Push/Pop。3.3 路由 Push/Pop 的语义transition 的 Push/Pop 与路由跳转语义对应router.pushUrl前进→ 触发 Push 转场新页面进 router.back返回→ 触发 Pop 转场当前页面出// 新页面只做 Push 进动画 Column() { /* ... */ } .transition({ type: TransitionType.Push, translate: { x: 300 } }, { duration: 300 }) // pushUrl 时新页面从右侧 x300 滑入到 x0 // back 时新页面消失瞬切没 Pop 转场四、与 if 条件渲染协同4.1 transition 依附 if 触发State showPause: boolean false; build() { Stack() { this.GameView() if (this.showPause) { // ← if 触发 this.PauseOverlay() .transition({ type: TransitionType.All, opacity: 0, scale: 0.8 }, { duration: 300 }) } } } // 出现showPausetruePauseOverlay 从 opacity 0/scale 0.8 淡入放大 // 消失showPausefalsePauseOverlay 淡出缩小到 opacity 0/scale 0.8完成后销毁关键经验transition 必须依附if条件渲染——if 控制出现/消失transition 补间过渡。没 if 的组件 transition 无意义。4.2 全局 Stack 嵌套弹窗「猫猫大作战」当前架构第 31 篇build() { Stack() { if (this.gameState GameState.IDLE) { this.MainMenuView() } else { this.GameView() } if (this.gameState GameState.PAUSED) { this.PauseOverlay() // ← 加 transition } if (this.gameState GameState.GAME_OVER) { this.GameOverOverlay() // ← 加 transition } } }机制全局 Stack 里多个if条件渲染每个弹窗加 transition切换 gameState 时弹窗淡入/淡出。4.3 主菜单与游戏页的切换if (this.gameState GameState.IDLE) { this.MainMenuView() .transition({ type: TransitionType.All, opacity: 0, translate: { x: -300 } }, { duration: 300 }) } else { this.GameView() .transition({ type: TransitionType.All, opacity: 0, translate: { x: 300 } }, { duration: 300 }) }效果开始游戏IDLE→PLAYING主菜单向左滑出淡出x: 0→-300, opacity 1→0游戏页从右滑入淡入x: 300→0, opacity 0→1。返回主菜单PLAYING→IDLE游戏页向右滑出主菜单从左滑入。关键经验主菜单和游戏页用相反方向的 translate——左出右进视觉有「推走」感。五、实战暂停/结束弹窗转场5.1 改造 PauseOverlay// 来源entry/src/main/ets/pages/Index.ets PauseOverlaytransition 改造后 Builder PauseOverlay() { Column() { Column() { Text(⏸️).fontSize(48).margin({ bottom: 12 }) Text(游戏暂停).fontSize(24).fontWeight(FontWeight.Bold).fontColor(#2C3E50).margin({ bottom: 8 }) Text(当前得分: ${this.score}).fontSize(16).fontColor(#7F8C8D).margin({ bottom: 24 }) Button(继续游戏) .width(80%).height(48) .fontSize(17).fontWeight(FontWeight.Bold) .fontColor(#FFFFFF).backgroundColor(#2ECC71) .borderRadius(24).margin({ bottom: 12 }) .onClick(() { this.resumeGame(); }) Button(重新开始) .width(80%).height(48) .fontSize(17) .fontColor(#FFFFFF).backgroundColor(#F39C12) .borderRadius(24).margin({ bottom: 12 }) .onClick(() { this.clearTimers(); this.startGame(); }) Button(返回主菜单) .width(80%).height(48) .fontSize(17) .fontColor(#7F8C8D).backgroundColor(#ECF0F1) .borderRadius(24) .onClick(() { this.clearTimers(); this.gameState GameState.IDLE; }) } .width(80%).padding(32) .backgroundColor(#FFFFFF).borderRadius(20) .shadow({ radius: 20, color: rgba(0,0,0,0.15), offsetY: 8 }) .alignItems(HorizontalAlign.Center) } .width(100%).height(100%) .backgroundColor(rgba(44, 62, 80, 0.6)) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) // ← 本篇重点弹窗进/出转场 .transition( { type: TransitionType.All, opacity: 0, scale: 0.8 }, { duration: 300, curve: Curve.EaseInOut } ) }5.2 改造 GameOverOverlayBuilder GameOverOverlay() { Column() { Column() { Text().fontSize(64).margin({ bottom: 16 }) Text(游戏结束).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#2C3E50).margin({ bottom: 24 }) Column() { Text(本局得分).fontSize(14).fontColor(#95A5A6) Text(this.score.toString()).fontSize(48).fontWeight(FontWeight.Bold).fontColor(#E67E22).margin({ bottom: 16 }) if (this.score this.highScore) { Text( 新纪录).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#E74C3C).margin({ bottom: 16 }) } }.margin({ bottom: 24 }) Row() { this.StatItem(最高连击, x${this.maxCombo}) this.StatItem(合并次数, ${this.mergeCount}次) this.StatItem(最高等级, ${this.highestLevel}) } .width(100%) .justifyContent(FlexAlign.SpaceEvenly) .margin({ bottom: 24 }) if (this.score this.highScore) { Text(历史最高: ${this.score}).fontSize(14).fontColor(#95A5A6).margin({ bottom: 16 }) } else { Text(历史最高: ${this.highScore}).fontSize(14).fontColor(#95A5A6).margin({ bottom: 16 }) } Button(再玩一次) .width(80%).height(48) .fontSize(17).fontWeight(FontWeight.Bold) .fontColor(#FFFFFF).backgroundColor(#3498DB) .borderRadius(24).margin({ bottom: 12 }) .onClick(() { this.clearTimers(); this.startGame(); }) Button(返回主菜单) .width(80%).height(48) .fontSize(17) .fontColor(#7F8C8D).backgroundColor(#ECF0F1) .borderRadius(24) .onClick(() { this.clearTimers(); this.gameState GameState.IDLE; }) } .width(85%).padding(32) .backgroundColor(#FFFFFF).borderRadius(20) .shadow({ radius: 20, color: rgba(0,0,0,0.15), offsetY: 8 }) .alignItems(HorizontalAlign.Center) } .width(100%).height(100%) .backgroundColor(rgba(44, 62, 80, 0.7)) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) // ← 本篇重点结束弹窗进/出转场 .transition( { type: TransitionType.All, opacity: 0, scale: 0.85, translate: { y: 50 } }, { duration: 400, curve: Curve.EaseOut } ) }5.3 改造 MainMenuView 和 GameViewBuilder MainMenuView() { Column() { /* ... 主菜单内容 ... */ } .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 GameView() { Column() { this.GameHUD() Column() { /* ... 棋盘内容 ... */ }.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 } ) }5.4 build 整体结构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%) }5.5 触发流程主菜单 → 游戏页点开始游戏gameState: IDLE → PLAYING。MainMenuView 的if变 false触发 transitionopacity 1→0, translate x: 0→-300300ms 向左滑出淡出。GameView 的if变 true触发 transitionopacity 0→1, translate x: 300→0300ms 从右滑入淡入。300ms 后主菜单销毁游戏页稳定显示。游戏页 → 暂停弹窗点暂停gameState: PLAYING → PAUSED。PauseOverlay 的if变 true触发 transitionopacity 0→1, scale 0.8→1300ms 淡入放大。GameView 的if还在PAUSED 非 IDLE不销毁游戏页保持渲染被弹窗遮盖。暂停弹窗 → 游戏页点继续gameState: PAUSED → PLAYING。PauseOverlay 的if变 false触发 transitionopacity 1→0, scale 1→0.8300ms 淡出缩小完成后销毁。GameView 露出继续游戏。六、transition vs Hero vs animation 对比6.1 三种动画 APIAPI范围触发适合animation单组件属性变化改属性自动循环改属性猫咪下落animateTo单组件显式触发主动调 API点击触发特效得分弹跳transitionif 控制的整组件if 出现/消失弹窗/页面进/出Hero跨页面单元素路由跳转列表→详情图过渡6.2 场景对照// 场景 A猫咪下落循环改 position→ animation .position({ x: cat.x, y: cat.y }) .animation({ duration: 100 }) // 场景 B得分弹跳点击触发一次→ animateTo animateTo({ duration: 300 }, () { this.scoreScale 1.5; }) // 场景 C暂停弹窗进/出if 控制→ transition本篇 if (this.gameState GameState.PAUSED) { this.PauseOverlay() .transition({ type: TransitionType.All, opacity: 0, scale: 0.8 }, { duration: 300 }) } // 场景 D点击猫咪跳详情页跨页面单元素→ Hero第 57 篇 .bindElements(cat_${cat.id}, this.heroAnimation)关键经验四种动画 API 各管一场景——循环改属性用 animation点击触发用 animateToif 进出用 transition跨页面单元素用 Hero。七、踩坑提示7.1 transition 用在非 if 组件// ❌ 错误组件一直存在transition 无触发时机 Column() { /* ... */ } .transition({ opacity: 0 }, { duration: 300 }) // 没进/出时机transition 无效 // ✅ 正确配合 if 控制出现/消失 if (this.showPause) { Column() { /* ... */ } .transition({ opacity: 0 }, { duration: 300 }) }7.2 忘 AnimationParam 第二参数// ❌ 错误只传 TranslateParam没 AnimationParam用默认时长可能太快 .transition({ opacity: 0 }) // ✅ 正确两个参数都传 .transition({ opacity: 0 }, { duration: 300, curve: Curve.EaseInOut })7.3 type 选错// ❌ 错误弹窗用 Push消失时瞬切没 Pop .transition({ type: TransitionType.Push, opacity: 0 }, { duration: 300 }) // 出现淡入消失瞬切不协调 // ✅ 正确弹窗用 All进/出都动 .transition({ type: TransitionType.All, opacity: 0, scale: 0.8 }, { duration: 300 })7.4 起始属性值设错// ❌ 错误opacity 起始设 1出现时从 1→1 无补间 .transition({ opacity: 1 }, { duration: 300 }) // ✅ 正确opacity 起始设 0出现时从 0→1 淡入 .transition({ opacity: 0 }, { duration: 300 })关键经验TranslateParam 的属性值是「起始值」——出现时从起始值补间到当前消失时从当前补间到起始值。7.5 忘 scale 分轴// ⚠️ scale: 0.8 是 {x: 0.8, y: 0.8} 双轴通常 OK .transition({ scale: 0.8 }, { duration: 300 }) // 要只缩一轴用 {x, y} 对象 .transition({ scale: { x: 1, y: 0.8 } }, { duration: 300 }) // 只缩 y八、调试技巧console.info在 gameState 切换时追 if 触发时机验证 transition。瞬切不补间排查检查是否在 if 内检查 TranslateParam 起始值是否设对检查 AnimationParam 是否传。消失没过渡排查检查 type 是否包含 PopAll 或 Pop检查 if 是否真的变 false。DevEco Animation Inspector查看 transition 过程。九、性能与最佳实践transition 必须依附 if 条件渲染——if 控制出现/消失transition 补间。弹窗用 TransitionType.All——进/出都动才协调单向场景才用 Push/Pop。TranslateParam 属性是「起始值」——出现从起始补间到当前消失从当前补间到起始。两个参数都传——TranslateParam AnimationParam别忘第二个。主菜单和游戏页用相反 translate——左出右进有「推走」感。四种动画各管一场景——循环改 animation点击 animateToif 进出 transition跨页面 Hero。十、阶段三进度51–58本篇是阶段三「交互与动画」第 8 篇篇主题核心要点51onTouch手势三阶段52onHover悬停反馈53onKeyEvent键盘/遥控按键54bindContextMenu上下文菜单55animateTo显式动画触发56animation隐式补间57Hero共享元素跨页面58本篇transitionif 进/出转场接下来第 59–60 篇会覆盖Spring 弹性物理、Hero Style Player。总结本篇我们从 transition 转场切入掌握了装饰绑定与 if 协同、TransitionType 推/拉/全弹窗用 All、TranslateParam 起始值出现从起始补到当前消失反向、与 animation/animateTo/Hero 的差异四大要点并给出了暂停/结束弹窗、主菜单/游戏页切换的完整 transition 改造代码。核心要点transition 依附 if弹窗用 All起始值是过渡起点四种动画各管一场景。下一篇我们将拆解 Spring——弹性物理动画。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etsArkUI transition 转场动画官方指南TransitionType 枚举官方文档HarmonyOS 页面转场最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md