Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示

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. 数据结构设计

▶ 网格单元类型
dart 复制代码
enum CellType { empty, wall, start, end, path, visited }
  • 清晰定义六种状态,便于 UI 渲染与逻辑判断。
▶ 二维坐标点
dart 复制代码
class Point {
  final int x, y;
  // 重写 == 和 hashCode,使其可作为 Map 的 key
}
  • 支持集合操作(如 Set<Point>)和映射(如 Map<Point, Point>),是实现 cameFrom 路径回溯的关键。

2. A* 核心逻辑 (_runAStar)

▶ 初始化
dart 复制代码
List<Point> openList = [start];
Set<Point> closedList = {};
Map<Point, Point> cameFrom = {}; // 记录父节点
Map<Point, double> gScore = {start: 0};
Map<Point, double> fScore = {start: _heuristic(start, end)};
▶ 主循环
  1. 选点 :从 openList 中选出 fScore 最小的节点 current
  2. 终止条件 :若 current == end,调用 _reconstructPath 回溯路径;
  3. 标记访问 :将 current 加入 closedList,并在 UI 上标记为蓝色(CellType.visited);
  4. 探索邻居 :遍历上下左右四个方向;
    • 跳过越界、墙壁或已在 closedList 中的点;
    • 计算临时 gScore,若更优则更新 gScorefScorecameFrom,并将邻居加入 openList
▶ 启发式函数(曼哈顿距离)
dart 复制代码
double _heuristic(Point a, Point b) {
  return (a.x - b.x).abs() + (a.y - b.y).abs();
}
  • 适用于只能上下左右移动的网格地图,保证可采纳性(Admissible),即不会高估实际代价,从而确保找到最优解。
▶ 路径重构
dart 复制代码
Future<void> _reconstructPath(Map<Point, Point> cameFrom, Point current) async {
  List<Point> totalPath = [];
  while (cameFrom.containsKey(current)) {
    current = cameFrom[current]!;
    totalPath.add(current);
  }
  // 从起点到终点反向遍历,再反转
  totalPath = totalPath.reversed.toList();
  // 逐个高亮为黄色
}

3. 异步与动画

  • await Future.delayed(...):在每次更新网格状态后暂停片刻,形成"探索"和"绘制路径"的动画效果;
  • setState(() {}):触发 UI 重绘,实时反映算法进展;
  • isRunning 标志:防止用户在运行时误操作,保证状态一致性。

四、UI/UX 设计亮点

1. 直观的色彩编码

颜色 含义
🟢 绿色 起点 (start)
🔴 红色 终点 (end)
⚪ 透明/白色 空地 (empty)
⚫ 灰色 障碍物 (wall)
🔵 蓝色(半透) 已探索节点 (visited)
🟡 黄色 最终路径 (path)

这种设计让用户一眼就能分辨算法的当前状态。

2. 交互式网格 (GridView.builder)

  • 每个格子都是一个 GestureDetector,点击即可切换空地/墙壁;
  • 使用 SliverGridDelegateWithFixedCrossAxisCount 精确控制列数,确保网格比例协调;
  • mainAxisSpacingcrossAxisSpacing 添加 1px 间隙,提升视觉清晰度。

3. 控制面板

三个功能按钮布局合理:

  • 重置:恢复初始布局(含默认起点终点);
  • 清空障碍:保留起点终点,仅移除墙壁,方便快速测试不同障碍配置;
  • 开始寻路:核心操作,运行时显示"运行中..."并禁用其他按钮。

五、教育价值与应用场景

教学场景

  • 算法课演示:动态展示 A* 如何"聪明地"避开障碍,优先向终点方向搜索;
  • 启发式函数对比 :可轻松扩展为支持欧几里得距离、对角线移动等,观察不同 h(n) 对搜索效率的影响;
  • 失败案例分析:当终点被完全包围时,应用会弹出提示"未找到可达路径!",帮助学生理解算法的局限性。

开发参考

  • 游戏 AI:可作为 NPC 自动寻路的基础模块;
  • 地图应用:室内导航、仓库机器人路径规划的简化模型;
  • 性能基准:通过调整网格大小,测试算法在不同规模问题下的表现。

六、总结

这段 Flutter 代码不仅是一个功能完整的 A* 寻路演示器,更是一个优秀的计算思维教学工具。它将复杂的图搜索过程分解为可视化的步骤,通过颜色、动画和即时反馈,极大地降低了学习门槛。

完整代码

bash 复制代码
import 'dart:ui' as ui;
import 'package:flutter/material.dart';

void main() {
  runApp(const PathFindingApp());
}

class PathFindingApp extends StatelessWidget {
  const PathFindingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      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) {
    return other is Point && x == other.x && y == other.y;
  }

  @override
  int get hashCode => Object.hash(x, y);
}

class AStarVisualizer extends StatefulWidget {
  const AStarVisualizer({super.key});

  @override
  State<AStarVisualizer> createState() => _AStarVisualizerState();
}

class _AStarVisualizerState extends State<AStarVisualizer> {
  static const int rows = 15;
  static const int cols = 20;
  late List<List<CellType>> grid;
  Point? start;
  Point? end;
  bool isRunning = false;

  @override
  void initState() {
    super.initState();
    _resetGrid();
  }

  void _resetGrid() {
    grid = List.generate(rows, (i) {
      return List.generate(cols, (j) {
        return CellType.empty;
      });
    });
    start = Point(1, 1);
    end = Point(rows - 2, cols - 2);
    grid[start!.x][start!.y] = CellType.start;
    grid[end!.x][end!.y] = CellType.end;
  }

  void _clearWalls() {
    for (int i = 0; i < rows; i++) {
      for (int j = 0; j < cols; j++) {
        if (grid[i][j] == CellType.wall) {
          grid[i][j] = CellType.empty;
        }
      }
    }
  }

  // A* 算法核心
  Future<void> _runAStar() async {
    if (start == null || end == null) return;

    setState(() {
      isRunning = true;
    });

    // 开启列表:待检查的节点
    List<Point> openList = [Point(start!.x, start!.y)];
    // 关闭列表:已检查的节点
    Set<Point> closedList = {};

    // 记录路径 (父节点映射)
    Map<Point, Point> cameFrom = {};

    // gScore: 从起点到该节点的实际代价
    Map<Point, double> gScore = {};
    gScore[start!] = 0;

    // fScore: 从起点经由该节点到终点的预估总代价
    Map<Point, double> fScore = {};
    fScore[start!] = _heuristic(start!, end!);

    while (openList.isNotEmpty) {
      // 1. 找到fScore最小的节点
      Point current = openList.reduce((a, b) {
        return (fScore[a] ?? double.infinity) < (fScore[b] ?? double.infinity)
            ? a
            : b;
      });

      // 2. 如果到达终点,重构路径
      if (current == end) {
        await _reconstructPath(cameFrom, current);
        setState(() {
          isRunning = false;
        });
        return;
      }

      // 3. 将当前节点移出开启列表,加入关闭列表
      openList.remove(current);
      closedList.add(current);

      // 可视化:标记为已访问
      if (grid[current.x][current.y] != CellType.start &&
          grid[current.x][current.y] != CellType.end) {
        grid[current.x][current.y] = CellType.visited;
        setState(() {});
        await Future.delayed(const Duration(milliseconds: 50));
      }

      // 4. 检查邻居 (上下左右)
      List<Point> neighbors = [
        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 neighbor in neighbors) {
        int x = neighbor.x;
        int y = neighbor.y;

        // 检查是否越界或为墙壁
        if (x < 0 ||
            x >= rows ||
            y < 0 ||
            y >= cols ||
            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(() {
      isRunning = false;
    });
  }

  // 启发式函数 (曼哈顿距离)
  double _heuristic(Point a, Point b) {
    return ((a.x - b.x).abs() + (a.y - b.y).abs()).toDouble();
  }

  // 重构路径
  Future<void> _reconstructPath(
      Map<Point, Point> cameFrom, Point current) async {
    List<Point> totalPath = [current];
    while (cameFrom.containsKey(current)) {
      current = cameFrom[current]!;
      totalPath.add(current);
    }
    totalPath = totalPath.reversed.toList();

    // 高亮显示路径
    for (Point p in totalPath) {
      if (p != start && p != end) {
        grid[p.x][p.y] = CellType.path;
        setState(() {});
        await Future.delayed(const Duration(milliseconds: 100));
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      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 row = index ~/ cols;
                int col = index % cols;
                bool isStart = Point(row, col) == start;
                bool isEnd = Point(row, col) == end;

                Color color = Colors.transparent;
                if (grid[row][col] == CellType.wall) {
                  color = Colors.grey;
                } else if (grid[row][col] == CellType.visited) {
                  color = Colors.blue.withOpacity(0.5);
                } else if (grid[row][col] == CellType.path) {
                  color = Colors.yellow;
                } else if (isStart) {
                  color = Colors.green;
                } else if (isEnd) {
                  color = Colors.red;
                }

                return GestureDetector(
                  onTap: () {
                    if (isRunning) return;
                    setState(() {
                      if (grid[row][col] == CellType.empty) {
                        grid[row][col] = CellType.wall;
                      } else if (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 ? '运行中...' : '开始寻路'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
相关推荐
小程故事多_802 小时前
Agent Infra核心技术解析:Sandbox sandbox技术原理、选型逻辑与主流方案全景
java·开发语言·人工智能·aigc
陈天伟教授2 小时前
人工智能应用- 语言处理:02.机器翻译:规则方法
人工智能·深度学习·神经网络·语言模型·自然语言处理·机器翻译
望舒5132 小时前
代码随想录day25,回溯算法part4
java·数据结构·算法·leetcode
千逐682 小时前
《Flutter for OpenHarmony:星轨天气的粒子化气象宇宙可视化系统》
flutter
人机与认知实验室2 小时前
一些容易被人工智能取代的职业
人工智能
C++ 老炮儿的技术栈2 小时前
Qt 编写 TcpClient 程序 详细步骤
c语言·开发语言·数据库·c++·qt·算法
茶栀(*´I`*)2 小时前
【NLP入门笔记】:自然语言处理基础与文本预处理
人工智能·自然语言处理·nlp
KYGALYX2 小时前
逻辑回归详解
算法·机器学习·逻辑回归
却道天凉_好个秋2 小时前
Tensorflow数据增强(三):高级裁剪
人工智能·深度学习·tensorflow