Flutter for OpenHarmony 可视化教学A* 寻路算法的交互式演示在人工智能、游戏开发和机器人导航等领域路径规划Pathfinding是一项基础而关键的技术。其中A*A-Star算法因其高效性与最优性成为最广为人知的启发式搜索算法之一。然而其背后的“开启列表”、“关闭列表”、“f g h”等概念对初学者而言仍显抽象。本文将深入解析一段完整的Flutter 应用代码该应用通过直观的网格界面、动态的颜色反馈和流畅的动画效果将 A* 算法的执行过程转化为一场沉浸式的可视化学习体验。你将看到如何用 Dart 实现经典的 A* 逻辑如何利用GridView构建交互式地图以及如何设计一个既能教学又能娱乐的寻路模拟器。 加入社区 欢迎加入开源鸿蒙跨平台开发者社区获取最新资源与技术支持 开源鸿蒙跨平台开发者社区完整效果一、应用概览交互式寻路沙盒该应用提供了一个15×20 的网格世界用户可以点击任意空白格子添加或移除灰色障碍物墙壁点击“开始寻路”启动 A* 算法从绿色起点到红色终点寻找最短路径实时观察蓝色区域表示算法正在探索的节点黄色路径即为最终结果一键重置恢复初始状态或仅清除障碍物进行新实验。核心目标让抽象的图搜索算法变得可见、可操作、可理解。二、A* 算法原理简述A* 是一种最佳优先搜索算法它通过评估函数f(n) g(n) h(n)来决定下一个要探索的节点g(n)从起点到当前节点n的实际代价本例中为步数h(n)从节点n到终点的启发式估计代价本例使用曼哈顿距离f(n)总预估代价值越小优先级越高。算法维护两个关键集合开启列表Open List待检查的候选节点关闭列表Closed List已处理完毕的节点。通过不断从开启列表中选择f值最小的节点进行扩展并更新邻居的代价最终找到一条从起点到终点的最短路径若存在。三、代码实现详解1. 数据结构设计▶ 网格单元类型enumCellType{empty,wall,start,end,path,visited}清晰定义六种状态便于 UI 渲染与逻辑判断。▶ 二维坐标点classPoint{finalint x,y;// 重写 和 hashCode使其可作为 Map 的 key}支持集合操作如SetPoint和映射如MapPoint, Point是实现cameFrom路径回溯的关键。2. A* 核心逻辑 (_runAStar)▶ 初始化ListPointopenList[start];SetPointclosedList{};MapPoint,PointcameFrom{};// 记录父节点MapPoint,doublegScore{start:0};MapPoint,doublefScore{start:_heuristic(start,end)};▶ 主循环选点从openList中选出fScore最小的节点current终止条件若current end调用_reconstructPath回溯路径标记访问将current加入closedList并在 UI 上标记为蓝色CellType.visited探索邻居遍历上下左右四个方向跳过越界、墙壁或已在closedList中的点计算临时gScore若更优则更新gScore、fScore和cameFrom并将邻居加入openList。▶ 启发式函数曼哈顿距离double_heuristic(Pointa,Pointb){return(a.x-b.x).abs()(a.y-b.y).abs();}适用于只能上下左右移动的网格地图保证可采纳性Admissible即不会高估实际代价从而确保找到最优解。▶ 路径重构Futurevoid_reconstructPath(MapPoint,PointcameFrom,Pointcurrent)async{ListPointtotalPath[];while(cameFrom.containsKey(current)){currentcameFrom[current]!;totalPath.add(current);}// 从起点到终点反向遍历再反转totalPathtotalPath.reversed.toList();// 逐个高亮为黄色}3. 异步与动画await Future.delayed(...)在每次更新网格状态后暂停片刻形成“探索”和“绘制路径”的动画效果setState(() {})触发 UI 重绘实时反映算法进展isRunning标志防止用户在运行时误操作保证状态一致性。四、UI/UX 设计亮点1. 直观的色彩编码颜色含义 绿色起点 (start) 红色终点 (end)⚪ 透明/白色空地 (empty)⚫ 灰色障碍物 (wall) 蓝色半透已探索节点 (visited) 黄色最终路径 (path)这种设计让用户一眼就能分辨算法的当前状态。2. 交互式网格 (GridView.builder)每个格子都是一个GestureDetector点击即可切换空地/墙壁使用SliverGridDelegateWithFixedCrossAxisCount精确控制列数确保网格比例协调mainAxisSpacing和crossAxisSpacing添加 1px 间隙提升视觉清晰度。3. 控制面板三个功能按钮布局合理重置恢复初始布局含默认起点终点清空障碍保留起点终点仅移除墙壁方便快速测试不同障碍配置开始寻路核心操作运行时显示“运行中…”并禁用其他按钮。五、教育价值与应用场景教学场景算法课演示动态展示 A* 如何“聪明地”避开障碍优先向终点方向搜索启发式函数对比可轻松扩展为支持欧几里得距离、对角线移动等观察不同h(n)对搜索效率的影响失败案例分析当终点被完全包围时应用会弹出提示“未找到可达路径”帮助学生理解算法的局限性。开发参考游戏 AI可作为 NPC 自动寻路的基础模块地图应用室内导航、仓库机器人路径规划的简化模型性能基准通过调整网格大小测试算法在不同规模问题下的表现。六、总结这段 Flutter 代码不仅是一个功能完整的 A* 寻路演示器更是一个优秀的计算思维教学工具。它将复杂的图搜索过程分解为可视化的步骤通过颜色、动画和即时反馈极大地降低了学习门槛。完整代码importdart:uias ui;importpackage:flutter/material.dart;voidmain(){runApp(const PathFindingApp());}class PathFindingApp extends StatelessWidget{const PathFindingApp({super.key});override Widget build(BuildContext context){returnMaterialApp(title:A* 寻路算法, theme: ThemeData.dark(), home: const AStarVisualizer(), debugShowCheckedModeBanner: false,);}}// 网格状态枚举 enum CellType{empty, wall, start, end, path, visited}// 二维坐标点 class Point{final int x, y;Point(this.x, this.y);override bool operator(Object other){returnother is Pointxother.xyother.y;}override int get hashCodeObject.hash(x, y);}class AStarVisualizer extends StatefulWidget{const AStarVisualizer({super.key});override StateAStarVisualizercreateState()_AStarVisualizerState();}class _AStarVisualizerState extends StateAStarVisualizer{static const int rows15;static const int cols20;late ListListCellTypegrid;Point? start;Point? end;bool isRunningfalse;override voidinitState(){super.initState();_resetGrid();}void_resetGrid(){gridList.generate(rows,(i){returnList.generate(cols,(j){returnCellType.empty;});});startPoint(1,1);endPoint(rows -2, cols -2);grid[start!.x][start!.y]CellType.start;grid[end!.x][end!.y]CellType.end;}void_clearWalls(){for(int i0;irows;i){for(int j0;jcols;j){if(grid[i][j]CellType.wall){grid[i][j]CellType.empty;}}}}// A* 算法核心 Futurevoid_runAStar()async{if(startnull||endnull)return;setState((){ isRunningtrue;});//开启列表待检查的节点 ListPointopenList[Point(start!.x,start!.y)];//关闭列表已检查的节点 SetPointclosedList{};//记录路径(父节点映射)MapPoint,PointcameFrom{};//gScore:从起点到该节点的实际代价 MapPoint,doublegScore{};gScore[start!]0;//fScore:从起点经由该节点到终点的预估总代价 MapPoint,doublefScore{};fScore[start!]_heuristic(start!,end!);while(openList.isNotEmpty){//1.找到fScore最小的节点 Point currentopenList.reduce((a,b){ return(fScore[a]??double.infinity)(fScore[b]??double.infinity)?a:b;});//2.如果到达终点重构路径 if(currentend){ await _reconstructPath(cameFrom,current);setState((){ isRunningfalse;});return;}//3.将当前节点移出开启列表加入关闭列表 openList.remove(current);closedList.add(current);//可视化标记为已访问 if(grid[current.x][current.y]!CellType.startgrid[current.x][current.y]!CellType.end){ grid[current.x][current.y]CellType.visited;setState((){});await Future.delayed(const Duration(milliseconds:50));}//4. 检查邻居(上下左右)ListPointneighbors[Point(current.x -1, current.y), Point(current.x 1, current.y), Point(current.x, current.y -1), Point(current.x, current.y 1),];for(Point neighborinneighbors){int xneighbor.x;int yneighbor.y;// 检查是否越界或为墙壁if(x0||xrows||y0||ycols||grid[x][y]CellType.wall){continue;}// 如果已经在关闭列表跳过if(closedList.contains(neighbor)){continue;}// 计算临时的gScore(从起点到邻居的代价)double tentativeGScore(gScore[current]?? double.infinity)1;// 如果该邻居不在开启列表中或者找到了更短的路径if(!openList.contains(neighbor)||tentativeGScore(gScore[neighbor]?? double.infinity)){// 记录路径 cameFrom[neighbor]current;gScore[neighbor]tentativeGScore;fScore[neighbor]tentativeGScore _heuristic(neighbor, end!);if(!openList.contains(neighbor)){openList.add(neighbor);}}}}// 没有找到路径 ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(未找到可达路径)),);setState((){ isRunningfalse;});}//启发式函数(曼哈顿距离)double _heuristic(Point a,Point b){ return((a.x-b.x).abs()(a.y-b.y).abs()).toDouble();}// 重构路径 Futurevoid_reconstructPath(MapPoint, PointcameFrom, Point current)async{ListPointtotalPath[current];while(cameFrom.containsKey(current)){currentcameFrom[current]!;totalPath.add(current);}totalPathtotalPath.reversed.toList();// 高亮显示路径for(Point pintotalPath){if(p!startp!end){grid[p.x][p.y]CellType.path;setState((){});await Future.delayed(const Duration(milliseconds:100));}}}override Widget build(BuildContext context){returnScaffold(appBar: AppBar(title: const Text(A* 寻路算法可视化),), body: Column(children:[const Text(绿色起点, Red终点, Gray障碍物, Blue探索中, Yellow路径), Expanded(child: GridView.builder(padding: const EdgeInsets.all(8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: cols, mainAxisSpacing:1, crossAxisSpacing:1,), itemCount: rows * cols, itemBuilder:(context, index){int rowindex ~/ cols;int colindex % cols;bool isStartPoint(row, col)start;bool isEndPoint(row, col)end;Color colorColors.transparent;if(grid[row][col]CellType.wall){colorColors.grey;}elseif(grid[row][col]CellType.visited){colorColors.blue.withOpacity(0.5);}elseif(grid[row][col]CellType.path){colorColors.yellow;}elseif(isStart){colorColors.green;}elseif(isEnd){colorColors.red;}returnGestureDetector(onTap:(){if(isRunning)return;setState((){if(grid[row][col]CellType.empty){grid[row][col]CellType.wall;}elseif(grid[row][col]CellType.wall){grid[row][col]CellType.empty;}});}, child: Container(decoration: BoxDecoration(color: color), child: const SizedBox(),),);},),), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children:[ElevatedButton.icon(onPressed:(){if(!isRunning){_resetGrid();setState((){});}}, icon: const Icon(Icons.refresh), label: const Text(重置),), ElevatedButton.icon(onPressed:(){if(!isRunning){_clearWalls();setState((){});}}, icon: const Icon(Icons.delete), label: const Text(清空障碍),), ElevatedButton.icon(onPressed: _runAStar, icon: const Icon(Icons.route), label: Text(isRunning ?运行中...:开始寻路),),],),],),);}}