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"); // 这里可以添加自定义逻辑,比如弹窗、跳转等 } }); } }
关键说明
- 图层顺序修复:通过
beforeLayerId: "road-label"将集群/非集群图层插入到道路标签层之前,确保不会被地图基础图层覆盖,适配DARK/LIGHT及自定义样式。 - 集群标记自动隐藏:统一使用ClusterSource管理点位,当点位被集群时,非集群图层的filter会自动过滤掉这些点位,实现隐藏。
- 自定义非集群图标:通过
style.addImage将本地图片加载到地图样式,再在SymbolLayer中指定iconImage为图片ID。 - 点击事件监听:通过
onFeatureSelected监听所有图层的要素点击,根据cluster属性区分集群与非集群点,分别处理逻辑。
配置要求
- 在
pubspec.yaml中添加assets资源:
flutter: assets: - assets/custom_marker.png
- 确保已配置Mapbox的access token(在Android/iOS配置文件中设置)
内容的提问来源于stack exchange,提问作者Andrey F
相关产品推荐
相关产品推荐

