首页 / 资讯中心 / 文章详情

Flutter 在 OpenHarmony 上做数独游戏:胜利弹窗实战全解析

Flutter 在 OpenHarmony 上做数独游戏:胜利弹窗实战全解析 ★ FEATURED ARTICLE
在跨端开发圈里摸爬滚打这些年我一直觉得“在某个新平台上跑通一个完整业务功能”才算真正入门。这次用 Flutter 在 OpenHarmony 上做数独游戏最让我上头的是“胜利弹窗”这个看似小的功能——它把 UI 渲染、动画调度、状态同步、平台通道全串了起来。标题里这几个关键词——Flutter、OpenHarmony、数独、胜利弹窗其实就是一条完整的技能链跨端框架怎么在新系统上落地游戏逻辑怎么写交互反馈怎么做得有质感。这篇文章我把整个实战过程摊开讲包括数独生成算法、手势输入、弹窗动画、鸿蒙适配的坑以及最后我把弹窗升级成“庆祝动效 社交分享”的完整经验。适合正在做 Flutter 跨端应用、或者准备把业务搬到 OpenHarmony 上的开发者尤其建议先看看弹窗那块——那是整场实战里最有代表性的一个点。1. 项目全局与设计思路拆解1.1 为什么选 Flutter 做 OpenHarmony 应用先说结论:如果你已经熟练使用 Flutter并且业务方要求应用必须上架 OpenHarmony 设备那用 Flutter for OpenHarmony 是目前综合成本最低的路径。原因有三个。第一Flutter 的渲染引擎是自绘的不依赖系统原生的控件树。这意味着同一套 UI 代码在 Android、iOS、OpenHarmony 上渲染结果基本一致。你不需要针对鸿蒙的 ArkUI 组件体系重新写一遍界面Flutter 的 Widget 树只管自己画系统只提供一个可承载画布的原生宿主。这个特性在跨端项目里极其重要因为 ArkUI 虽然能力不错但它的声明式写法、状态管理和 Flutter 差异很大如果两边都维护一套 UI工作量直接翻倍。第二Flutter for OpenHarmony 已经在适配层做了大量工作包括手势事件映射、文本输入、平台通道等基础能力。我的实际感受是对于纯 UI 和游戏类逻辑迁移成本非常低。数独游戏没有太多复杂的系统 API 依赖棋盘绘制、触摸反馈、弹窗动画全是 Flutter 自身的能力真正需要走平台通道的只有一个震动反馈和分享功能整体上非常顺畅。第三社区和工具链已经能支撑实际开发。我用的 Flutter SDK 版本基于 OpenHarmony 官方适配分支配合 DevEco Studio 的构建链路能够完成从创建工程、编译 HAP 到真机调试的完整闭环。虽然个别 API 还有兼容性问题但整体可用性比我预想中好很多。当然选型也要看场景。如果你们团队从零开始、没有 Flutter 基础并且目标只面向鸿蒙设备那直接学 ArkUI 可能更合理。如果团队已经有一支 Flutter 团队那毫无疑问走 Flutter for OpenHarmony。用一句行业里常说的话跨端框架的价值不在于“一次编写处处运行”的完美承诺而在于把业务逻辑和 UI 的大部分代码沉淀到一个团队能持续维护的统一技术栈里。1.2 胜利弹窗在游戏中的角色定位弹窗是整局游戏的“情绪终点站”。玩家用手指在九宫格里填下最后一个数字系统检测到所有格子都满足数独规则那一刻的信息传递绝不能只是一行干巴巴的文字——“恭喜你完成了”。那会让玩家一秒钟从成就感的顶峰跌回“哦完事”的平淡。弹窗的正确打开方式应该是先让玩家感受到“我赢了”的情绪高潮再提供“继续看看我是怎么赢的”“要不要再来一局”这些后续动作。我在这款数独 App 里把胜利弹窗设计成三个层次。第一层是物理反馈也就是调用鸿蒙系统级的震动。这个效果虽然简单但极其有效玩家手指落下最后一个数字时手机轻轻震一下能瞬间强化“我刚刚完成了一个关键动作”的感知。第二层是视觉爆炸效果用一个从中心向四周扩散的粒子动画铺满整个屏幕金色粒子像烟花一样散开中间缓缓浮起“通关”两个字再过渡到完整的弹窗卡片。第三层是信息展示区显示本次通关耗时、用了多少提示、错误次数、难度级别让玩家在兴奋的余韵里能快速回顾这局游戏的表现。设计弹窗时我给自己定了一条原则动画要让玩家等待但绝不能让他等太久。如果粒子动画持续超过两秒玩家的耐心会迅速下降成就感受到影响。所以我控制在 1.3 秒左右完成动效展示然后弹窗卡片自然浮现整个过程顺滑而克制。1.3 整体代码架构与状态流这个项目我采用了非常务实的 MVC 架构没有引入额外的状态管理库因为数独游戏的页面状态本身不算复杂Bloc、Provider 这种重量级方案反而让代码更绕。Model 层负责核心的数据结构和算法SudokuGenerator负责生成有效的数独题目和答案SudokuSolver负责求解GameState表示当前游戏进度包括棋盘数据、错误计数、提示次数、计时。View 层用 Flutter Widget 树组织SudokuBoard负责渲染棋盘ControlPanel是底部数字选择器和功能按钮VictoryDialog就是胜利弹窗。Controller 层则是 GameController 这个 ChangeNotifier 子类它承担了游戏的调度逻辑——处理用户输入时校验对错、维护计时器、判断胜利条件、触发弹窗。状态流转的核心逻辑是这样的。玩家点击一个空格的数字按钮Controller 判断该数字填入后是否与所在行、列、宫内的已有数字冲突。没有冲突就写入棋盘如果有冲突则在对应格子上标记一个“错误状态”错误次数加一。每成功填入一个数字就检查是否填完了全部空格如果所有格子都填满且无冲突则触发victory状态弹窗展示。弹窗展示的数据来源是 GameState 统计的耗时和错误次数这块我是通过 ChangeNotifier 的监听机制传进去的弹窗只做一个展示组件不直接访问 GameState。从整个项目来看胜利弹窗像是整场演出的谢幕环节。如果前面的“演出”——棋盘交互、数字校验、计时逻辑都做得不好谢幕再华丽也没用。所以后面几节先讲清楚数独核心逻辑和交互最后再重点拆解谢幕这个高光环节。2. 数独核心逻辑与棋盘实现2.1 数独生成的算法选型与实现数独题目的生成我选择了市面上主流的“全量生成后挖洞”方案。即先通过回溯算法填充一个完整的 9x9 数独答案矩阵再根据难度随机挖掉一定数量的数字作为待填空位。为什么要先填充完整答案再挖洞因为数独的规则约束很强——每行、每列、每个 3x3 宫格内都必须是 1 到 9 且不重复。如果直接用随机数往空棋盘里填题很容易出现前置数字互相冲突导致后面无解。而先通过回溯生成完整解答再去掉一些数字能保证留下的题目一定有解。回溯填充的核心流程用一个简化代码说明:bool _fillBoard(ListListint board, int row, int col) { if (row 9) return true; if (col 9) return _fillBoard(board, row 1, 0); final numbers Listint.generate(9, (i) i 1)..shuffle(); for (var num in numbers) { if (_isValid(board, row, col, num)) { board[row][col] num; if (_fillBoard(board, row, col 1)) return true; board[row][col] 0; } } return false; }注意numbers.shuffle()这一步它决定了生成题目的多样性。如果不打乱顺序每次生成的数独题目会高度相似游戏很快失去新鲜感。洗牌后每次回溯都会沿着不同的随机路径搜索最终生成完全不重复的终盘。_isValid函数只检查行、列、宫三个维度复杂度为 O(1)代码不复杂这是数独逻辑最基础的部分bool _isValid(ListListint board, int row, int col, int num) { for (int i 0; i 9; i) { if (board[row][i] num) return false; if (board[i][col] num) return false; } int startRow (row ~/ 3) * 3; int startCol (col ~/ 3) * 3; for (int r startRow; r startRow 3; r) { for (int c startCol; c startCol 3; c) { if (board[r][c] num) return false; } } return true; }挖洞环节有个容易踩的坑随机挖掉的数字必须能通过逻辑推断填回去否则玩家可能面对一道需要“猜”的题游戏体验直接归零。我采用的策略是挖空之后立即用求解器验证唯一解如果无解或多解就换一个位置继续尝试。这个验证会带来一些性能消耗实测下来在数独这种 9x9 的规模上求解一次大约几毫秒完全在可接受范围内。难度调节我是这样控制的简单难度挖掉 36 个数字中等难度挖掉 45 个困难难度挖掉 54 个。注意挖洞数量只是一个粗略指标真正决定难度的是挖洞位置的分布。比如硬挖掉一排数字剩下的题目可能逻辑简单但挖在关键转折点上就可能让难度陡增。新手期我为了提高开发效率只按数量随机挖结果生成过一道“困难”题目实际上手后 30 秒就解完了。后续我用了更严谨的方案——按玩家平均解题时间评估难度先用求解器统计每个空格需要多少步逻辑推理才能确定再把不同推理深度的空格组合进不同难度档位。2.2 棋盘 UI 的绘制与手势交互棋盘我用的是一个自绘 Widget内部用CustomPaint绘制网格线和粗分隔线数字则用 TextPainter 一个个画到画布上。这样做的好处是性能非常可控整个棋盘一次绘制完成不涉及几十个 Text Widget 的布局计算。网格线粗细的视觉效果很影响整体质感我踩过一个细节的坑细线用 0.8 逻辑像素渲染在部分鸿蒙设备上会显示成 1 像素偏粗后来我把所有网格线统一画为 1 像素并让粗线达到普通格线的三倍粗细视图层次马上清晰多了。玩家交互分两步第一步点选要填的空格高亮该格及其所在行、列、宫方便玩家快速定位关联区域第二步点底部数字栏或系统数字键盘输入数字。底部的数字选择面板我用 1 到 9 的数字按钮横向排列每次点击时 Controller 更新选区然后由棋盘 Widget 重绘。为了避免整个棋盘频繁刷新我用了RepaintBoundary隔离棋盘和控制器区域输入数字时只有棋盘部分重绘。触摸手势的映射用GestureDetector的onTapDown实现获取到相对棋盘的局部坐标后根据格子宽高计算行列int col (localPosition.dx / cellSize).floor(); int row (localPosition.dy / cellSize).floor();这里有边界判断的细节要处理好如果手指按在棋盘的边缘位置计算出来的行列可能是 9越界了就需要裁掉if (col 0 || col 8 || row 0 || row 8) return;2.3 胜利条件检测与计时系统胜利条件的检测逻辑极其简单每次填入数字后遍历整个棋盘只要没有空格即视为胜利。实际代码会比一句话稍微复杂一点因为“没有空格”必须满足“所有空格都被填充”且“所有填充都符合规则”。我在 Controller 里维护了一个filledCount数字每成功填入一个空格就加一减少一个擦除数字就减一。这样判断胜利只需要比较filledCount 81不需要每次 O(81) 全盘扫描。计时系统我用了Timer.periodic实现每一秒更新一次状态。有两点处理经验游戏从第一笔有效输入才开始计时。玩家打开页面发呆 3 分钟那 3 分钟不应该算作游戏时间。这个体验细节非常重要直接关系到胜利弹窗展示的时间成绩是否合理。应用退到后台时需要暂停计时回到前台再恢复。我用WidgetsBindingObserver监听应用生命周期在didChangeAppLifecycleState里对 Timer 做暂停与重启。漏掉这一步的话玩家切出去回个消息再回来看游戏时间已经跑了很多分钟弹窗里的胜利耗时直接穿帮。3. 胜利弹窗的完整实现3.1 弹窗整体结构与分层设计胜利弹窗我没有用 Flutter 自带的showDialog直接套 AlertDialog而是自己写了一个全屏层叠的 Widget用Overlay插入到应用顶层。为什么这么设计因为我想在弹窗出现前播放一整段粒子动画动画阶段并不需要一个对话框卡片出来抢夺注意力Overlay 能让我自由控制任意 Widget 的展示顺序和层级。弹窗的结构分为三层。第一层是遮罩层用一个从透明渐变到 45% 黑色透明的全屏 Container 盖住棋盘。这个遮罩也给后面的动效提供了干净的深色背景金色粒子在深色背景上比在亮色棋盘上更显眼。第二层是粒子动画层用 CustomPainter 绘制若干金色粒子每帧更新粒子的位置和透明度。粒子效果不需要引入第三方特效库自己控制一个粒子系统并不复杂代码量也少。第三层是核心信息卡片层一张圆角卡片浮在粒子动画上方卡片顶部一个大大的“恭喜通关”下面分为统计区和操作区。统计区显示三种信息通关耗时、错误次数、提示使用次数操作区是两个按钮“再来一局”和“返回主页”。3.2 动画实现从粒子爆炸到卡片浮入粒子动画是整个弹窗视觉效果的重头戏具体实现我把它拆成三个阶段。阶段一粒子从屏幕中心向外爆发。用 Random 生成每个粒子的初始角度、初速度、尺寸和旋转速度然后通过 AnimationController 驱动 0 到 1 的进度值在 build 时遍历粒子数组计算每个粒子当前的位置和透明度class Particle { final double angle; final double velocity; final double size; final double rotation; final double initialAlpha; final Color color; } void _updateParticles(double t) { for (var p in _particles) { double distance p.velocity * t * 0.6; double x center.dx cos(p.angle) * distance; double y center.dy sin(p.angle) * distance; double alpha p.initialAlpha * (1 - t); // 粒子逐渐变淡 // 绘制时使用 x,y,alpha } }阶段二动画进行到 0.6 时卡片使用弹性曲线从下方滑入。这里我用了一个 CurvedAnimation 配合Interval让卡片在粒子还在消散的时候就开始入场形成层次感。卡片入场的效果是透明度从 0 到 1同时 y 轴偏移从 40 像素回到 0配合Curves.easeOutBack能产生轻微的过冲回弹没有弹窗的生硬感。阶段三动画进行到 1.0 时把 Overlay 里的内容替换成稳定的弹窗状态同时禁用遮罩层的点击退出避免玩家不小心点一下遮罩就把弹窗关了。动画实现过程中最大的性能陷阱是粒子的数量。我之前试过 400 个粒子在性能较好的测试设备上很流畅但换到中低端鸿蒙设备上Paint 数量过多直接掉帧。后来把粒子数量降到 120 个同时保证粒子的绘制操作集中在 Canvas 内部效果上没有明显的牺牲帧率反而稳定了。3.3 胜利弹窗的代码实现骨架把核心代码骨架贴出来这个结构可以直接抄作业class VictoryOverlay extends StatefulWidget { final GameResult result; final VoidCallback onRestart; final VoidCallback onHome; const VictoryOverlay({super.key, required this.result, required this.onRestart, required this.onHome}); override StateVictoryOverlay createState() _VictoryOverlayState(); } class _VictoryOverlayState extends StateVictoryOverlay with SingleTickerProviderStateMixin { late final AnimationController _controller; late final ListParticle _particles; override void initState() { super.initState(); _controller AnimationController(vsync: this, duration: const Duration(milliseconds: 1400)); _initParticles(); _controller.forward(); // 触发震动 _triggerHapticFeedback(); } void _initParticles() { final random Random(); _particles List.generate(120, (i) { final angle random.nextDouble() * 2 * pi; final velocity 80 random.nextDouble() * 180; final size 2 random.nextDouble() * 4; return Particle( angle: angle, velocity: velocity, size: size, rotation: random.nextDouble() * 0.2, initialAlpha: 0.8 random.nextDouble() * 0.2, color: _randomGoldColor(random), ); }); } override void dispose() { _controller.dispose(); super.dispose(); } override Widget build(BuildContext context) { return Positioned.fill( child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Stack( fit: StackFit.expand, children: [ Container(color: Colors.black.withOpacity(0.45)), CustomPaint(painter: ParticlePainter(_particles, _controller.value)), if (_controller.value 0.6) _buildVictoryCard(_controller.value), ], ); }, ), ); } }3.4 与 ChangeNotifier 状态流的联动弹窗触发的时机在 Controller 中判断。当filledCount达到 81 时Controller 先停止计时器然后设置gameResult数据最后通知 listeners。界面上一个专门监听状态的组件捕获到胜利状态后把VictoryOverlay插入 Overlayvoid _onStateChanged() { if (_gameController.isVictory !_victoryShown) { _victoryShown true; _showVictoryOverlay(); } } void _showVictoryOverlay() { final overlay Overlay.of(context); late OverlayEntry entry; entry OverlayEntry( builder: (context) VictoryOverlay( result: _gameController.gameResult, onRestart: () { entry.remove(); _gameController.reset(); setState(() { _victoryShown false; }); }, onHome: () { entry.remove(); Navigator.of(context).pop(); }, ), ); overlay.insert(entry); }这里有个小细节必须处理Overlay 插入的位置如果不在当前页面的树上方弹窗会被页面下方的内容覆盖。所以我调用Overlay.of(context)时传的是 MaterialApp 根节点必要时用Overlay.of(context, rootOverlay: true)强制使用根 Overlay保证弹窗一定在最顶层。还有一个重复触发的风险。因为展示弹窗后状态还是胜利状态如果不做_victoryShown防重复判断玩家重新进入页面时可能弹窗直接又弹出来一次。我是在 Controller 的 reset 方法里同步清除_victoryShown并恢复计时状态这样每局独立且不会互相干扰。3.5 按钮交互与事件回调弹窗的两个按钮动作很直观但实现时有交互状态需要保持。玩家点击“再来一局”应立刻关闭弹窗、重置棋盘、重置计时器、重新生成题目。这里我还加了一个过渡动画——棋盘内容在 0.3 秒内淡出再淡入避免突然换一张新棋盘让人觉得“游戏跳帧了”。“返回主页”的处理更简单移除 Overlay 后直接Navigator.pop回到关卡列表页。这里有另一个小细节如果玩家是从关卡列表点进来的返回后应该刷新列表页的进度展示。我通过 Navigator.pop 返回时携带了一个结果值Navigator.of(context).pop(true);列表页通过await Navigator.push(...)的返回值判断是否刷新。这个小改动让“返回主页”不只是关页面而是带着进度更新回到入口体验闭环了。4. OpenHarmony 环境适配与构建验证4.1 工程创建与鸿蒙适配配置用 Flutter for OpenHarmony 创建工程有两种方式我用的是官方推荐的命令行方式flutter create --platformsohos sudoku_app命令执行后会在工程目录生成ohos子目录这就是鸿蒙应用的原生壳工程。如果项目已经存在可以用flutter create --platformsohos .给现有 Flutter 工程补充鸿蒙平台支持。生成的 ohos 目录里需要确认几个关键配置module.json5是鸿蒙模块的配置文件里面需要声明应用包名、版本号、能力标签。我遇到过默认配置的 ability 类型不正确导致无法拉起页面的问题后来按官方模板改成type: page即可。MainAbility的OnWindowStageCreated方法里需要加载 Flutter 引擎。官方模板生成的时候这一块通常已经帮你写好了但要检查 Flutter 引擎的初始化参数是否正确。权限配置。数独游戏要用震动反馈需要在module.json5里申请ohos.permission.VIBRATE。漏加这个权限在真机上调用震动接口时不会报异常但震动就是不响非常隐蔽。还有一点鸿蒙应用的图标和名称都在 ohos 目录的资源配置里设置。如果准备发布到应用市场图标尺寸、前景背景分离这些都要按规范调整这个和 Android 的 mipmap 规范逻辑类似。4.2 编译打包与真机调试的注意事项Flutter for OpenHarmony 目前最稳的构建方式是通过 DevEco Studio 打开 ohos 目录然后使用 IDE 的构建能力。我在命令行直接跑flutter build ohos也可以但部分版本对 Gradle 和 SDK 路径的组合要求比较严格命令行构建报错的概率偏高。所以我最终确定的开发流程是Flutter 代码改动后回到 DevEco Studio 里同步工程并构建。本地模拟器调试时有个常见问题模拟器不支持震动反馈接口调用调用相关 API 会静默失败。为此我在震动封装层做了一个 try-catch 和模拟器检测异常情况下直接跳过震动不阻断弹窗逻辑。在真机上则一切正常。4.3 弹窗相关平台差异的实测记录这部分我实际测下来最值得记录的差异有三个。第一Overlay 层级在鸿蒙上的表现和 Android 上略有不同。Android 上我用根 Overlay 妥妥地盖住页面没问题鸿蒙上如果 Flutter 引擎嵌在非全屏的 Ability 中根 Overlay 的尺寸可能比视口小。解决方式是在 MainAbility 里把窗口设为全屏或者直接使用FlutterViewController的标准尺寸。我采用全屏窗口方案后问题消失。第二动画帧率差异。同样是 120 个粒子在部分鸿蒙中端设备上能感觉到轻微的掉帧。排查后发现问题不在粒子数量而在 CustomPaint 没有设置isComplex和willChange导致每次重绘都参与布局阶段的计算缓存。把这两个参数按属性设为 true 后帧率明显提升。第三文本渲染差异。鸿蒙系统字体没有 Android 的 Roboto 和 iOS 的 SF 字体中文字体默认可能是鸿蒙自家的 HarmonyOS Sans并且 FontWeight 渲染效果偏细。弹窗卡片上的标题如果用了偏细的字重在鸿蒙上没有 Android 上显得“精神”。我把标题的字重调整为 FontWeight.w600 后视觉上更一致。5. 实战问题排查与体验优化5.1 胜利弹窗不弹出的排查过程这是我开发过程中浪费最多时间的一个问题。最初实现好胜利检测后我在模拟器上正常通关弹窗如期出现。换到鸿蒙真机上怎么都不弹窗而棋盘已经全部填满状态也显示 victory。排查过程分了三步才定位。第一步怀疑是状态监听没有触发。我在_onStateChanged里加了日志填完最后一格时日志正常打印说明状态监听没有问题问题出在_showVictoryOverlay的执行环节。第二步排查 Overlay 插入。我打印了Overlay.of(context)是否为空结果没空。但插入OverlayEntry后界面上没有任何反应。这时我怀疑 OverlayEntry 的 builder 在构建时抛了异常但控制台没有打印错误因为 Overlay 的异常被吞了。第三步是绕开 Overlay我直接把 VictoryOverlay 用Navigator.push的全屏路由方式插入试试。结果全屏路由一旦弹出整个棋盘和状态框架就被盖住了也不符合动画层叠的需求。最终查明了根因我在overlay.insert(entry)时当前 context 的 Overlay 和实际显示的 Overlay 不是同一个。因为在鸿蒙的 Flutter 容器里路由栈中可能同时存在多个 Overlay我拿到的是最内层路由的 Overlay而实际渲染的另有其人。把代码改成Overlay.of(context, rootOverlay: true)后问题直接消失。排查这次问题让我深刻体会到一件事跨端开发里“同一个 API 在不同平台上的边界条件不同”才是最大的坑逻辑本身往往没问题。5.2 动画掉帧与内存占用的优化记录粒子动画在低端设备上有两个指标需要关注帧率和内存占用。优化前我用 400 个粒子每帧都要计算位置并绘制中低端设备上帧率掉到 45fps 左右。优化后的方案是粒子数降到 120。粒子从屏幕中心向四周散开时超过屏幕边界的直接跳过绘制不再计算透明度。Canvas 上绘制粒子时启用Paint.isAntiAlias false因为粒子本身是圆形色块抗锯齿带来的视觉收益很小但性能开销明显。把粒子绘制和卡片绘制的 RepaintBoundary 分开粒子动画进行时卡片区域不重建。优化后帧率稳定在 58 到 60fps内存基线从 220MB 降到 170MB 左右。对于数独这种轻量游戏来说这个表现已经完全合格。5.3 玩家体验细节与后续优化方向游戏功能完整跑通后我额外做了三项体验细节优化。第一项是“撤销”功能。我加了一个撤销栈记录玩家最近 50 步操作用来支持撤销。胜利弹窗出现后撤销栈也会被清空避免玩家带着上一局的操作痕迹进入下一局造成数据错乱。第二项是“提示”功能。当玩家卡住时点击提示按钮系统自动在空白格中选择一个可确定数字的位置并填充正确值。提示次数有限制所以弹窗里的统计会展示“提示次数”供玩家回顾。这里有个非常关键的逻辑细节提示填充的数字必须是唯一解数字否则即使填充了后续的推理也可能走入死胡同。第三项是排行榜与后续扩展计划。当前版本我没有做排行榜后面可以基于本局耗时和错误次数计算一个评分然后接入平台通道把成绩上传。实际上我这次已经预留了GameResult数据类后续加排行榜时只需接入即可。还可以把胜利场景从弹窗扩展到“关卡解锁”动画——通关后棋盘背景逐渐变换成下一关预览交互上更有连贯性。6. 实战总结与个人体会我个人在实际操作中的体会是Flutter for OpenHarmony 的成熟度已经足够支撑一个完整的小游戏应用开发真正的瓶颈不在框架能力而在开发者的平台适配意识。胜利弹窗虽然只是整个项目的一个功能但它把渲染、动画、状态流、平台通道、生命周期这几个 Flutter 开发的硬骨头全部串起来了。要是有人问我学 Flutter 跨端开发应该从什么项目入手我会毫不犹豫地推荐做一个带完整弹窗闭环的数独游戏。最后再分享一个小技巧弹窗卡片在 Overlay 里完成展示后我建议不要立即销毁 OverlayEntry而是让它保留几百毫秒再销毁否则在鸿蒙上偶发会出现“弹窗卡片闪一下然后消失”的问题。这个现象我排查了很久最后发现是 OverlayEntry 在动画完成前的最后一帧被回收导致的。保留几百毫秒再移除既不影响视觉也彻底规避了这个偶发问题。做跨端开发就是这样细节坑一个接一个但摸清规律后写代码的底气和愉悦感是实实在在的。
阅读完成 · 觉得有帮助?
咨询建站