Flutter GridPaper组件单个容器着色异常问题求助——路径可视化工具中起点/终点/障碍物颜色区分实现
Hey Robsen, let's fix this issue step by step. The core problem here is that you're using a single list to track all activated cells without differentiating their types (start, end, obstacle), and your block variable isn't managed as reactive state—plus, when you switch to start mode, you're re-rendering every existing activated cell as green instead of just marking new clicks as the start point.
1. Create a Node Type System
First, we need a way to distinguish between different cell types. Let's define an enum to represent these clearly:
enum NodeType { obstacle, start, end }
Instead of a List<Offset>, use a Map<Offset, NodeType> to track which offset corresponds to which node type. This gives us explicit separation between obstacles, start points, and end points.
2. Make block Reactive State
Your current block variable is a plain class member—changing it won't trigger a UI rebuild. Convert it to a useState hook so Flutter knows to update the UI when its value changes:
final block = useState(NodeType.obstacle); // Default to obstacle mode
3. Update the Toggle Logic
Modify the _toggle function to handle different node types, and add logic to ensure only one start/end point exists (standard for pathfinding apps):
void _toggle(Offset offset) { final currentType = block.value; // Clear existing start/end if we're setting a new one if (currentType == NodeType.start) { _nodes.value.removeWhere((key, value) => value == NodeType.start); } else if (currentType == NodeType.end) { _nodes.value.removeWhere((key, value) => value == NodeType.end); } // Toggle the cell: if it's already the current type, remove it; else set it if (_nodes.value.containsKey(offset) && _nodes.value[offset] == currentType) { _nodes.value.remove(offset); } else { _nodes.value[offset] = currentType; } // Trigger state update to refresh UI _nodes.value = {..._nodes.value}; }
4. Rewrite the Rendering Logic
Instead of switching on block to render all cells the same, iterate through the Map and render each cell based on its stored NodeType:
List<Widget> gridContainerMap(ValueNotifier<Map<Offset, NodeType>> _nodes) { final widgets = [Container(color: Colors.white)]; _nodes.value.forEach((offset, type) { Color color; switch (type) { case NodeType.obstacle: color = Colors.black; break; case NodeType.start: color = Colors.green; break; case NodeType.end: color = Colors.red; break; } widgets.add(Positioned( left: offset.dx * cellSize, top: offset.dy * cellSize, width: cellSize, height: cellSize, child: ColoredBox(color: color), )); }); return widgets; }
Full Modified Code
Here's the complete working code with all these changes:
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; enum NodeType { obstacle, start, end } void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final appTitle = 'Path Finder'; final Color gridColor = Colors.lightBlue[100]!; @override Widget build(BuildContext context) { return MaterialApp( title: appTitle, home: MyHomePage(title: appTitle), ); } } class MyHomePage extends HookWidget { final double cellSize = 20.0; final String title; GlobalKey _key = GlobalKey(); MyHomePage({ Key? key, required this.title, }) : super(key: key); @override Widget build(BuildContext context) { final _nodes = useState<Map<Offset, NodeType>>({}); final block = useState(NodeType.obstacle); // Reactive state for current mode void _toggle(Offset offset) { final currentType = block.value; // Ensure only one start/end exists if (currentType == NodeType.start) { _nodes.value.removeWhere((key, value) => value == NodeType.start); } else if (currentType == NodeType.end) { _nodes.value.removeWhere((key, value) => value == NodeType.end); } // Toggle the cell if (_nodes.value.containsKey(offset) && _nodes.value[offset] == currentType) { _nodes.value.remove(offset); } else { _nodes.value[offset] = currentType; } _nodes.value = {..._nodes.value}; } return Scaffold( appBar: AppBar(title: Text(title)), body: GestureDetector( onTapDown: (details) => _toggle(details.localPosition ~/ cellSize), child: GridPaper( child: Stack( children: gridContainerMap(_nodes), ), color: Colors.lightBlue[100]!, interval: cellSize, divisions: 1, subdivisions: 1, ), ), drawer: Drawer( child: ListView( padding: EdgeInsets.zero, children: <Widget>[ const DrawerHeader( child: Text('Einstellungen'), decoration: BoxDecoration( color: Colors.blue, ), ), ListTile( title: const Text('Startpunkt'), onTap: () { block.value = NodeType.start; Navigator.pop(context); }, ), ListTile( title: const Text('Ziel'), onTap: () { block.value = NodeType.end; Navigator.pop(context); }, ), ListTile( title: const Text('Pathfinding-Algorithm'), onTap: () { Navigator.pop(context); }, ), ], ), ), ); } List<Widget> gridContainerMap(ValueNotifier<Map<Offset, NodeType>> _nodes) { final widgets = [Container(color: Colors.white)]; _nodes.value.forEach((offset, type) { Color color; switch (type) { case NodeType.obstacle: color = Colors.black; break; case NodeType.start: color = Colors.green; break; case NodeType.end: color = Colors.red; break; } widgets.add(Positioned( left: offset.dx * cellSize, top: offset.dy * cellSize, width: cellSize, height: cellSize, child: ColoredBox(color: color), )); }); return widgets; } }
Key Improvements Explained
- Node Type Enum: Clearly categorizes each cell's purpose, making it easy to extend later (like adding visited/path nodes for pathfinding algorithms).
- Reactive State: Using
useStatefor both_nodesandblockensures the UI updates correctly when either value changes. - Single Start/End: The toggle logic prevents multiple start or end points, which is a standard requirement for pathfinding visualizers.
- Targeted Rendering: Each cell is rendered based on its stored type, so switching modes only affects new clicks—existing cells keep their original type.
内容的提问来源于stack exchange,提问作者Robsen

