在Flutter中使用mapbox_maps_flutter:^2.0.0实现地图图形绘制的手势问题
解决方案:基于mapbox_maps_flutter实现圆/正方形绘制
核心思路
不要用Flutter原生GestureDetector包裹Mapbox控件(会覆盖地图原生手势),而是使用Mapbox自带的GestureListeners监听拖拽手势,这样既能保留地图滚动、点击等原生交互,又能精准捕获绘制操作。绘制流程分为初始点确认→实时预览→最终确认三个阶段,避免重复生成图层。
实现步骤
1. 定义状态变量
import 'dart:math'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:flutter/material.dart'; enum DrawingMode { none, circle, square } class MapDrawingPage extends StatefulWidget { const MapDrawingPage({super.key}); @override State<MapDrawingPage> createState() => _MapDrawingPageState(); } class _MapDrawingPageState extends State<MapDrawingPage> { final _mapKey = GlobalKey<MapWidgetState>(); DrawingMode _drawingMode = DrawingMode.none; Position? _initialPoint; // 圆心/正方形起始角点 Position? _currentPoint; // 当前拖拽点(用于实时预览) String? _tempLayerId; // 临时预览图层ID List<Layer> _drawnLayers = []; // 已完成的绘制图层 // ... 后续方法写在这里 }
2. 配置MapWidget的手势监听
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("施工区域标记")), body: Column( children: [ // 绘制模式切换按钮 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () => setState(() => _drawingMode = DrawingMode.circle), child: const Text("绘制圆形"), ), const SizedBox(width: 16), ElevatedButton( onPressed: () => setState(() => _drawingMode = DrawingMode.square), child: const Text("绘制正方形"), ), const SizedBox(width: 16), ElevatedButton( onPressed: () => setState(() => _drawingMode = DrawingMode.none), child: const Text("结束绘制"), ), ], ), Expanded( child: MapWidget( key: _mapKey, styleUri: StyleURI.mapboxStreets, gestureListeners: GestureListeners( // 点击确定初始点 onTap: (TapGestureContext context) { if (_drawingMode != DrawingMode.none) { _initialPoint = context.point.coordinates; _currentPoint = _initialPoint; setState(() {}); } }, // 拖拽开始时记录初始点(如果未通过点击确认) onPanStart: (PanStartGestureContext context) { if (_drawingMode != DrawingMode.none && _initialPoint == null) { _initialPoint = context.point.coordinates; _currentPoint = _initialPoint; } }, // 拖拽更新时实时刷新预览图形 onPanUpdate: (PanUpdateGestureContext context) { if (_drawingMode != DrawingMode.none && _initialPoint != null) { _currentPoint = context.point.coordinates; _updatePreviewShape(); } }, // 拖拽结束时确认绘制,生成永久图层 onPanEnd: (PanEndGestureContext context) { if (_drawingMode != DrawingMode.none && _initialPoint != null && _currentPoint != null) { _finalizeShape(); // 重置临时状态 _initialPoint = null; _currentPoint = null; _tempLayerId = null; setState(() {}); } }, ), ), ), ], ), ); }
3. 实现实时预览逻辑
// 更新临时预览图形 void _updatePreviewShape() async { final mapController = await _mapKey.currentState?.controller; if (mapController == null || _initialPoint == null || _currentPoint == null) return; // 移除上一次的临时图层 if (_tempLayerId != null) { await mapController.style.removeLayer(_tempLayerId!); await mapController.style.removeSource(_tempLayerId!); } final layerId = 'temp-${DateTime.now().microsecondsSinceEpoch}'; _tempLayerId = layerId; if (_drawingMode == DrawingMode.circle) { final radius = calculateDistance(_initialPoint!, _currentPoint!); if (radius < 5) return; // 忽略小于5米的圆 // 添加圆的GeoJSON源和图层 final source = GeoJsonSource( data: Feature( geometry: Point(coordinates: _initialPoint!), properties: {'radius': radius}, ).toJson(), ); await mapController.style.addSource(layerId, source); final circleLayer = CircleLayer( id: layerId, source: layerId, circleRadius: Expression.number(get: ['get', 'radius']), circleColor: Expression.rgba(255, 0, 0, 0.3), circleStrokeColor: Expression.rgb(255, 0, 0), circleStrokeWidth: Expression.number(2), ); await mapController.style.addLayer(circleLayer); } else if (_drawingMode == DrawingMode.square) { // 计算正方形对角边界 final minLng = min(_initialPoint!.lng, _currentPoint!.lng); final maxLng = max(_initialPoint!.lng, _currentPoint!.lng); final minLat = min(_initialPoint!.lat, _currentPoint!.lat); final maxLat = max(_initialPoint!.lat, _currentPoint!.lat); // 添加正方形的GeoJSON源和图层 final polygon = Polygon( coordinates: [ [ Position(minLng, minLat), Position(maxLng, minLat), Position(maxLng, maxLat), Position(minLng, maxLat), Position(minLng, minLat), ] ], ); final source = GeoJsonSource( data: Feature(geometry: polygon).toJson(), ); await mapController.style.addSource(layerId, source); final fillLayer = FillLayer( id: layerId, source: layerId, fillColor: Expression.rgba(0, 0, 255, 0.3), fillOutlineColor: Expression.rgb(0, 0, 255), fillOutlineWidth: Expression.number(2), ); await mapController.style.addLayer(fillLayer); } }
4. 实现最终绘制确认逻辑
// 生成永久绘制图层 void _finalizeShape() async { final mapController = await _mapKey.currentState?.controller; if (mapController == null || _initialPoint == null || _currentPoint == null) return; final layerId = 'drawn-${DateTime.now().microsecondsSinceEpoch}'; if (_drawingMode == DrawingMode.circle) { final radius = calculateDistance(_initialPoint!, _currentPoint!); if (radius < 5) return; final source = GeoJsonSource( data: Feature( geometry: Point(coordinates: _initialPoint!), properties: {'radius': radius}, ).toJson(), ); await mapController.style.addSource(layerId, source); final circleLayer = CircleLayer( id: layerId, source: layerId, circleRadius: Expression.number(get: ['get', 'radius']), circleColor: Expression.rgba(255, 0, 0, 0.3), circleStrokeColor: Expression.rgb(255, 0, 0), circleStrokeWidth: Expression.number(2), ); await mapController.style.addLayer(circleLayer); _drawnLayers.add(circleLayer); } else if (_drawingMode == DrawingMode.square) { final minLng = min(_initialPoint!.lng, _currentPoint!.lng); final maxLng = max(_initialPoint!.lng, _currentPoint!.lng); final minLat = min(_initialPoint!.lat, _currentPoint!.lat); final maxLat = max(_initialPoint!.lat, _currentPoint!.lat); final polygon = Polygon( coordinates: [ [ Position(minLng, minLat), Position(maxLng, minLat), Position(maxLng, maxLat), Position(minLng, maxLat), Position(minLng, minLat), ] ], ); final source = GeoJsonSource( data: Feature(geometry: polygon).toJson(), ); await mapController.style.addSource(layerId, source); final fillLayer = FillLayer( id: layerId, source: layerId, fillColor: Expression.rgba(0, 0, 255, 0.3), fillOutlineColor: Expression.rgb(0, 0, 255), fillOutlineWidth: Expression.number(2), ); await mapController.style.addLayer(fillLayer); _drawnLayers.add(fillLayer); } } // 计算两点间距离(米) double calculateDistance(Position p1, Position p2) { const earthRadius = 6371000; final dLat = (p2.lat - p1.lat) * pi / 180; final dLng = (p2.lng - p1.lng) * pi / 180; final a = sin(dLat / 2) * sin(dLat / 2) + cos(p1.lat * pi / 180) * cos(p2.lat * pi / 180) * sin(dLng / 2) * sin(dLng / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return earthRadius * c; }
关键说明
- 避免手势冲突:使用Mapbox原生
GestureListeners,只有在指定绘制模式时才处理拖拽手势,其余时间保留地图原生交互。 - 防止重复生成图层:通过
onPanEnd监听拖拽结束事件,仅在此时生成永久图层,临时预览图层会在每次拖拽更新时移除重建。 - 实时反馈:拖拽过程中动态更新临时图层,让用户直观看到绘制效果。
内容的提问来源于stack exchange,提问作者Prvind Panday
相关产品推荐
相关产品推荐

