You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 03:37:04