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

mapbox_maps_flutter集群与标记问题及功能实现技术咨询

解决Mapbox Flutter集群图层问题的完整实现

核心修复点

  • 放弃PointAnnotationManager,统一用VectorSource + SymbolLayer实现集群与非集群点,避免图层冲突与标记无法隐藏问题
  • 添加图层时指定插入位置,确保集群/非集群图层在地图基础图层之上
  • 加载自定义图片资源到地图样式,替换默认非集群点图标
  • 通过onFeatureSelected监听集群与非集群点的点击事件

完整代码示例

import 'package:flutter/material.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

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

  @override
  State<MapboxClusterExample> createState() => _MapboxClusterExampleState();
}

class _MapboxClusterExampleState extends State<MapboxClusterExample> {
  late MapboxMapController mapController;
  final String _customMarkerImageId = "custom-marker";
  final String _clusterSourceId = "cluster-source";
  final String _clusterLayerId = "cluster-layer";
  final String _unclusteredLayerId = "unclustered-layer";

  // 模拟点位数据
  final List<Map<String, dynamic>> _points = [
    {"type": "Point", "coordinates": [116.397228, 39.9075]},
    {"type": "Point", "coordinates": [116.407228, 39.9075]},
    {"type": "Point", "coordinates": [116.397228, 39.9175]},
    {"type": "Point", "coordinates": [121.473701, 31.230416]},
    {"type": "Point", "coordinates": [121.483701, 31.230416]},
  ];

  @override
  void dispose() {
    mapController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: MapboxMap(
        styleUri: MapboxStyles.DARK, // 测试深色样式
        onMapCreated: _onMapCreated,
        initialCameraPosition: const CameraPosition(
          target: Point(coordinates: Position(116.397228, 39.9075)),
          zoom: 10,
        ),
      ),
    );
  }

  void _onMapCreated(MapboxMapController controller) async {
    mapController = controller;

    // 1. 加载自定义标记图片到地图样式
    await _loadCustomMarkerImage();

    // 2. 创建集群数据源
    await _createClusterSource();

    // 3. 添加集群图层
    await _addClusterLayer();

    // 4. 添加非集群点图层
    await _addUnclusteredLayer();

    // 5. 监听图层点击事件
    _setupClickListeners();
  }

  Future<void> _loadCustomMarkerImage() async {
    // 从assets加载图片(需在pubspec.yaml中配置assets路径)
    final ByteData data = await DefaultAssetBundle.of(context).load("assets/custom_marker.png");
    final Uint8List bytes = data.buffer.asUint8List();
    await mapController.style.addImage(_customMarkerImageId, bytes, true);
  }

  Future<void> _createClusterSource() async {
    // 创建GeoJSON格式的点位数据
    final geoJson = {
      "type": "FeatureCollection",
      "features": _points.map((point) => {
        "type": "Feature",
        "geometry": point,
        "properties": {"id": DateTime.now().millisecondsSinceEpoch.toString()} // 给每个点位添加唯一标识
      }).toList(),
    };

    // 创建集群数据源
    await mapController.style.addSource(ClusterSource(
      id: _clusterSourceId,
      data: geoJson,
      cluster: true,
      clusterRadius: 50,
      clusterMaxZoom: 14, // 超过该缩放层级则取消集群
    ));
  }

  Future<void> _addClusterLayer() async {
    // 添加集群图层,插入到"road-label"图层之上,避免被地图元素覆盖
    await mapController.style.addLayer(
      CircleLayer(
        id: _clusterLayerId,
        sourceId: _clusterSourceId,
        filter: Exp.eq(Exp.get("cluster"), true),
        circleColor: [
          Exp.step(Exp.get("point_count"), "#51bbd6", [10, "#f1f075"], [100, "#f28cb1"])
        ],
        circleRadius: [Exp.step(Exp.get("point_count"), 20, [10, 25], [100, 30])],
        circleStrokeWidth: 2,
        circleStrokeColor: "#ffffff",
      ),
      // 指定插入位置,确保图层在地图标签之上
      beforeLayerId: "road-label",
    );

    // 添加集群数量文本图层
    await mapController.style.addLayer(
      SymbolLayer(
        id: "${_clusterLayerId}-text",
        sourceId: _clusterSourceId,
        filter: Exp.eq(Exp.get("cluster"), true),
        textField: Exp.get("point_count_abbreviated"),
        textSize: 12,
        textColor: "#ffffff",
        textIgnorePlacement: true,
        textAllowOverlap: true,
      ),
      beforeLayerId: "road-label",
    );
  }

  Future<void> _addUnclusteredLayer() async {
    // 添加非集群点图层,使用自定义图片
    await mapController.style.addLayer(
      SymbolLayer(
        id: _unclusteredLayerId,
        sourceId: _clusterSourceId,
        filter: Exp.not(Exp.eq(Exp.get("cluster"), true)),
        iconImage: _customMarkerImageId,
        iconSize: 0.5, // 调整图片大小
        iconAllowOverlap: true,
        iconIgnorePlacement: true,
        interactive: true, // 开启交互
      ),
      beforeLayerId: "road-label",
    );
  }

  void _setupClickListeners() {
    // 监听要素选中事件
    mapController.onFeatureSelected.listen((event) {
      if (event.features.isEmpty) return;

      final feature = event.features.first;
      final properties = feature.properties;

      if (properties?["cluster"] == true) {
        // 处理集群点击:展开集群(缩放至合适层级)
        final clusterId = properties?["cluster_id"];
        mapController.style.getClusterExpansionZoom(_clusterSourceId, clusterId).then((zoom) {
          mapController.camera.easeTo(CameraOptions(
            center: feature.geometry?.coordinates as Position,
            zoom: zoom,
            duration: 500,
          ));
        });
        print("点击集群,包含${properties?["point_count"]}个点位");
      } else {
        // 处理非集群点点击
        final pointId = properties?["id"];
        print("点击非集群点,ID:$pointId");
        // 这里可以添加自定义逻辑,比如弹窗、跳转等
      }
    });
  }
}

关键说明

  1. 图层顺序修复:通过beforeLayerId: "road-label"将集群/非集群图层插入到道路标签层之前,确保不会被地图基础图层覆盖,适配DARK/LIGHT及自定义样式。
  2. 集群标记自动隐藏:统一使用ClusterSource管理点位,当点位被集群时,非集群图层的filter会自动过滤掉这些点位,实现隐藏。
  3. 自定义非集群图标:通过style.addImage将本地图片加载到地图样式,再在SymbolLayer中指定iconImage为图片ID。
  4. 点击事件监听:通过onFeatureSelected监听所有图层的要素点击,根据cluster属性区分集群与非集群点,分别处理逻辑。

配置要求

  • 在pubspec.yaml中添加assets资源:
flutter:
  assets:
    - assets/custom_marker.png
  • 确保已配置Mapbox的access token(在Android/iOS配置文件中设置)

内容的提问来源于stack exchange,提问作者Andrey F

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 16:24:54