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

Mapbox Flutter:要素点击弹出弹窗实现方法求助

Flutter mapbox_gl ~0.16.0 点击要素弹窗实现问题

我正在用mapbox_gl: ~0.16.0开发Flutter应用,现在没法实现用户点击地图要素时显示弹窗的功能。没找到插件自带的合适弹窗控件或类似功能,不知道该怎么加这个特性。

简化代码

视图代码

MapboxMap(
  onMapCreated: (mController) {
    mapboxMapController = mController;
    controller.onMapCreated(mController);
  },
  zoomGesturesEnabled: true,
  accessToken: "<MAPBOX ACCESS TOKEN>",
  initialCameraPosition: const CameraPosition(
      target:
          LatLng(47.59051426075817, -107.77329953835964),
      zoom: 15.0),
  trackCameraPosition: true,
  onCameraIdle: () {
    controller.getView(mapboxMapController);
  },
  styleString:
      "mapbox://styles/mapbox/outdoors-v12?optimize=true&attribution=false&logo=false",
  rotateGesturesEnabled: false,
  tiltGesturesEnabled: false,
);

控制器代码

void onMapCreated(MapboxMapController mapboxMap) async {
  var bounds = await mapboxMap.getVisibleRegion();

  double northeastLat = bounds.northeast.latitude;
  double northeastLng = bounds.northeast.longitude;
  double northwestLat = bounds.northeast.latitude;
  double northwestLng = bounds.southwest.longitude;
  double southeastLat = bounds.southwest.latitude;
  double southeastLng = bounds.northeast.longitude;
  double southwestLat = bounds.southwest.latitude;
  double southwestLng = bounds.southwest.longitude;

  String queryString =
      "myAPI.php?northeastLat=$northeastLat&northeastLng=$northeastLng&"
      "northwestLat=$northwestLat&northwestLng=$northwestLng&"
      "southeastLat=$southeastLat&southeastLng=$southeastLng&"
      "southwestLat=$southwestLat&southwestLng=$southwestLng&"
      "zmLvl=15&uid=100";

  Map<String, dynamic> mappedData = {};

  var servResJson = await sendDataServer(queryString, mappedData);
  var servRes = jsonDecode(jsonEncode(servResJson.data));
  geojson = Map<String, Object>.from(servRes);

  await Future.wait([
    mapboxMap.removeLayer("fills-count"),
    mapboxMap.removeLayer("fills-circle"),
    mapboxMap.removeLayer("fills-fills"),
    mapboxMap.removeSource("fills"),
    mapboxMap.addSource(
      "fills",
      GeojsonSourceProperties(
        data: geojson,
      ),
    ),
    mapboxMap.addFillLayer(
      "fills",
      "fills-fills",
      FillLayerProperties(
        fillColor: [
          "match",
          ["get", "status"],
          1, statusColors[1], // Status 1 - Blue
          2, statusColors[2], // Status 2 - Yellow
          3, statusColors[3], // Status 3 - Green
          4, statusColors[4], // Status 4 - Transparent
          convertColorToRgba(
              const Color.fromARGB(128, 246, 246, 246), 0), // Default color
        ],
        fillAntialias: true,
        fillOpacity: 0.6,
        fillOutlineColor: 'rgba(0, 0, 0, 1)',
      ),
      filter: ["has", "status"],
      enableInteraction: true,
    ),
  ]);
  
  update();
}

API返回的GeoJSON示例

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "id": "56019",
            "status": 2,
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [
                            -107.774000467,
                            47.598100673
                        ],
                        [
                            -107.765000467,
                            47.598100673
                        ],
                        [
                            -107.765000467,
                            47.589100673
                        ],
                        [
                            -107.774000467,
                            47.589100673
                        ],
                        [
                            -107.774000467,
                            47.598100673
                        ]
                    ]
                ]
            },
            "properties": {
                "dbId": "56019",
                "status": 2,
                "uid": 100,
                "isBlocked": 0,
                "id": "56019",
                "left": -107.774000467,
                "top": 47.598100673,
                "right": -107.765000467,
                "bottom": 47.589100673
            }
        }
        // 更多要素省略
    ]
}

已尝试的代码

我能通过下面的代码获取到要素ID,但不知道不用自定义对话框的话,能不能用插件自带的弹窗实现显示ID的效果(效果是点击要素后弹出包含ID的气泡框):

mapboxMap.onFeatureTapped.add((id, point, coordinates) {
  if (id != null) {
    debugPrint(
        'Tapped feature with ID - $id, Point - $point, Coordinates - $coordinates');
  }
});

解决方案

首先明确:mapbox_gl: ~0.16.0本身没有自带的气泡弹窗组件,你需要自己实现悬浮弹窗,或者用Flutter原生的对话框结合坐标转换来完成,下面是两种可行方案:

方案1:自定义地图气泡弹窗

  1. 用Stack包裹MapboxMap和弹窗,让两者处于同一层级。
  2. 维护状态变量存储弹窗的显示状态、位置和要素ID。
  3. 在onFeatureTapped回调中,把经纬度坐标转换成屏幕坐标,更新弹窗位置并显示。

示例代码片段:

// 状态变量
LatLng? tappedCoordinate;
String? tappedFeatureId;
Offset? popupOffset;

// 视图结构
Stack(
  children: [
    MapboxMap(
      // 原有配置...
      onFeatureTapped: (id, point, coordinates) async {
        if (id != null) {
          // 把经纬度转成屏幕坐标
          final screenOffset = await mapboxMapController.convertLatLngToScreenCoordinate(coordinates);
          setState(() {
            tappedCoordinate = coordinates;
            tappedFeatureId = id;
            popupOffset = screenOffset;
          });
        }
      },
      onCameraMove: () {
        // 地图移动时隐藏弹窗
        setState(() {
          tappedCoordinate = null;
          tappedFeatureId = null;
          popupOffset = null;
        });
      },
    ),
    // 自定义气泡弹窗
    if (popupOffset != null && tappedFeatureId != null)
      Positioned(
        left: popupOffset!.dx - 60,
        top: popupOffset!.dy - 70, // 向上偏移避免遮挡点击点
        child: Container(
          width: 120,
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(8),
            boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 4, offset: Offset(0,2))],
          ),
          child: Text(
            'ID: $tappedFeatureId',
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 14),
          ),
        ),
      ),
  ],
);

方案2:用原生对话框快速实现

如果不需要气泡样式,直接用Flutter原生showDialog弹出对话框显示ID,代码更简洁:

mapboxMap.onFeatureTapped.add((id, point, coordinates) {
  if (id != null) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('要素信息'),
        content: Text('ID: $id'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('关闭'),
          ),
        ],
      ),
    );
  }
});

内容的提问来源于stack exchange,提问作者sayantan poddar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 13:58:14