Skip to content

Commit 244cd4c

Browse files
authored
[google_maps_flutter] Marker clustering support (flutter#4319)
Adds the initial support for the marker clustering for Android, iOS and Web platforms. Clustering is implemented using native google maps utils libraries for [Android](https://github.com/googlemaps/android-maps-utils), [iOS](https://github.com/googlemaps/google-maps-ios-utils) and [Web](https://github.com/googlemaps/js-markerclusterer). This PR is created from previous PR: flutter/plugins#6752 Resolves flutter#26863 **Android**: ![image](https://github.com/flutter/packages/assets/5219613/09b84a8e-f05b-4c71-8808-4043a25201f6) **iOS**: ![image](https://github.com/flutter/packages/assets/5219613/0859cf12-2e8c-4106-b7a7-cd4922a7dd1e) **Web**: ![image](https://github.com/flutter/packages/assets/5219613/9269d22a-1908-4c2c-a1ab-70addb06d0f2)
1 parent 8af79fb commit 244cd4c

File tree

11 files changed

+642
-2
lines changed

11 files changed

+642
-2
lines changed

packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.9.0
2+
3+
* Adds clustering support.
4+
15
## 2.8.0
26

37
* Adds support for heatmap layers.

packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_inspector.dart

+102
Original file line numberDiff line numberDiff line change
@@ -533,4 +533,106 @@ void runTests() {
533533
expect(myLocationButtonEnabled, true);
534534
});
535535
}, skip: !isIOS);
536+
537+
testWidgets('marker clustering', (WidgetTester tester) async {
538+
final Key key = GlobalKey();
539+
const int clusterManagersAmount = 2;
540+
const int markersPerClusterManager = 5;
541+
final Map<MarkerId, Marker> markers = <MarkerId, Marker>{};
542+
final Set<ClusterManager> clusterManagers = <ClusterManager>{};
543+
544+
for (int i = 0; i < clusterManagersAmount; i++) {
545+
final ClusterManagerId clusterManagerId =
546+
ClusterManagerId('cluster_manager_$i');
547+
final ClusterManager clusterManager =
548+
ClusterManager(clusterManagerId: clusterManagerId);
549+
clusterManagers.add(clusterManager);
550+
}
551+
552+
for (final ClusterManager cm in clusterManagers) {
553+
for (int i = 0; i < markersPerClusterManager; i++) {
554+
final MarkerId markerId =
555+
MarkerId('${cm.clusterManagerId.value}_marker_$i');
556+
final Marker marker = Marker(
557+
markerId: markerId,
558+
clusterManagerId: cm.clusterManagerId,
559+
position: LatLng(
560+
kInitialMapCenter.latitude + i, kInitialMapCenter.longitude));
561+
markers[markerId] = marker;
562+
}
563+
}
564+
565+
final Completer<GoogleMapController> controllerCompleter =
566+
Completer<GoogleMapController>();
567+
568+
await pumpMap(
569+
tester,
570+
GoogleMap(
571+
key: key,
572+
initialCameraPosition: kInitialCameraPosition,
573+
clusterManagers: clusterManagers,
574+
markers: Set<Marker>.of(markers.values),
575+
onMapCreated: (GoogleMapController googleMapController) {
576+
controllerCompleter.complete(googleMapController);
577+
},
578+
),
579+
);
580+
581+
final GoogleMapController controller = await controllerCompleter.future;
582+
583+
final GoogleMapsInspectorPlatform inspector =
584+
GoogleMapsInspectorPlatform.instance!;
585+
586+
for (final ClusterManager cm in clusterManagers) {
587+
final List<Cluster> clusters = await inspector.getClusters(
588+
mapId: controller.mapId, clusterManagerId: cm.clusterManagerId);
589+
final int markersAmountForClusterManager = clusters
590+
.map<int>((Cluster cluster) => cluster.count)
591+
.reduce((int value, int element) => value + element);
592+
expect(markersAmountForClusterManager, markersPerClusterManager);
593+
}
594+
595+
// Remove markers from clusterManagers and test that clusterManagers are empty.
596+
for (final MapEntry<MarkerId, Marker> entry in markers.entries) {
597+
markers[entry.key] = _copyMarkerWithClusterManagerId(entry.value, null);
598+
}
599+
600+
await pumpMap(
601+
tester,
602+
GoogleMap(
603+
key: key,
604+
initialCameraPosition: kInitialCameraPosition,
605+
clusterManagers: clusterManagers,
606+
markers: Set<Marker>.of(markers.values)),
607+
);
608+
609+
for (final ClusterManager cm in clusterManagers) {
610+
final List<Cluster> clusters = await inspector.getClusters(
611+
mapId: controller.mapId, clusterManagerId: cm.clusterManagerId);
612+
expect(clusters.length, 0);
613+
}
614+
});
615+
}
616+
617+
Marker _copyMarkerWithClusterManagerId(
618+
Marker marker, ClusterManagerId? clusterManagerId) {
619+
return Marker(
620+
markerId: marker.markerId,
621+
alpha: marker.alpha,
622+
anchor: marker.anchor,
623+
consumeTapEvents: marker.consumeTapEvents,
624+
draggable: marker.draggable,
625+
flat: marker.flat,
626+
icon: marker.icon,
627+
infoWindow: marker.infoWindow,
628+
position: marker.position,
629+
rotation: marker.rotation,
630+
visible: marker.visible,
631+
zIndex: marker.zIndex,
632+
onTap: marker.onTap,
633+
onDragStart: marker.onDragStart,
634+
onDrag: marker.onDrag,
635+
onDragEnd: marker.onDragEnd,
636+
clusterManagerId: clusterManagerId,
637+
);
536638
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:math';
6+
7+
import 'package:flutter/material.dart';
8+
import 'package:google_maps_flutter/google_maps_flutter.dart';
9+
10+
import 'page.dart';
11+
12+
/// Page for demonstrating marker clustering support.
13+
class ClusteringPage extends GoogleMapExampleAppPage {
14+
/// Default Constructor.
15+
const ClusteringPage({Key? key})
16+
: super(const Icon(Icons.place), 'Manage clustering', key: key);
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return const ClusteringBody();
21+
}
22+
}
23+
24+
/// Body of the clustering page.
25+
class ClusteringBody extends StatefulWidget {
26+
/// Default Constructor.
27+
const ClusteringBody({super.key});
28+
29+
@override
30+
State<StatefulWidget> createState() => ClusteringBodyState();
31+
}
32+
33+
/// State of the clustering page.
34+
class ClusteringBodyState extends State<ClusteringBody> {
35+
/// Default Constructor.
36+
ClusteringBodyState();
37+
38+
/// Starting point from where markers are added.
39+
static const LatLng center = LatLng(-33.86, 151.1547171);
40+
41+
/// Marker offset factor for randomizing marker placing.
42+
static const double _markerOffsetFactor = 0.05;
43+
44+
/// Offset for longitude when placing markers to different cluster managers.
45+
static const double _clusterManagerLongitudeOffset = 0.1;
46+
47+
/// Maximum amount of cluster managers.
48+
static const int _clusterManagerMaxCount = 3;
49+
50+
/// Amount of markers to be added to the cluster manager at once.
51+
static const int _markersToAddToClusterManagerCount = 10;
52+
53+
/// Fully visible alpha value.
54+
static const double _fullyVisibleAlpha = 1.0;
55+
56+
/// Half visible alpha value.
57+
static const double _halfVisibleAlpha = 0.5;
58+
59+
/// Google map controller.
60+
GoogleMapController? controller;
61+
62+
/// Map of clusterManagers with identifier as the key.
63+
Map<ClusterManagerId, ClusterManager> clusterManagers =
64+
<ClusterManagerId, ClusterManager>{};
65+
66+
/// Map of markers with identifier as the key.
67+
Map<MarkerId, Marker> markers = <MarkerId, Marker>{};
68+
69+
/// Id of the currently selected marker.
70+
MarkerId? selectedMarker;
71+
72+
/// Counter for added cluster manager ids.
73+
int _clusterManagerIdCounter = 1;
74+
75+
/// Counter for added markers ids.
76+
int _markerIdCounter = 1;
77+
78+
/// Cluster that was tapped most recently.
79+
Cluster? lastCluster;
80+
81+
void _onMapCreated(GoogleMapController controllerParam) {
82+
setState(() {
83+
controller = controllerParam;
84+
});
85+
}
86+
87+
@override
88+
void dispose() {
89+
super.dispose();
90+
}
91+
92+
void _onMarkerTapped(MarkerId markerId) {
93+
final Marker? tappedMarker = markers[markerId];
94+
if (tappedMarker != null) {
95+
setState(() {
96+
final MarkerId? previousMarkerId = selectedMarker;
97+
if (previousMarkerId != null && markers.containsKey(previousMarkerId)) {
98+
final Marker resetOld = markers[previousMarkerId]!
99+
.copyWith(iconParam: BitmapDescriptor.defaultMarker);
100+
markers[previousMarkerId] = resetOld;
101+
}
102+
selectedMarker = markerId;
103+
final Marker newMarker = tappedMarker.copyWith(
104+
iconParam: BitmapDescriptor.defaultMarkerWithHue(
105+
BitmapDescriptor.hueGreen,
106+
),
107+
);
108+
markers[markerId] = newMarker;
109+
});
110+
}
111+
}
112+
113+
void _addClusterManager() {
114+
if (clusterManagers.length == _clusterManagerMaxCount) {
115+
return;
116+
}
117+
118+
final String clusterManagerIdVal =
119+
'cluster_manager_id_$_clusterManagerIdCounter';
120+
_clusterManagerIdCounter++;
121+
final ClusterManagerId clusterManagerId =
122+
ClusterManagerId(clusterManagerIdVal);
123+
124+
final ClusterManager clusterManager = ClusterManager(
125+
clusterManagerId: clusterManagerId,
126+
onClusterTap: (Cluster cluster) => setState(() {
127+
lastCluster = cluster;
128+
}),
129+
);
130+
131+
setState(() {
132+
clusterManagers[clusterManagerId] = clusterManager;
133+
});
134+
_addMarkersToCluster(clusterManager);
135+
}
136+
137+
void _removeClusterManager(ClusterManager clusterManager) {
138+
setState(() {
139+
// Remove markers managed by cluster manager to be removed.
140+
markers.removeWhere((MarkerId key, Marker marker) =>
141+
marker.clusterManagerId == clusterManager.clusterManagerId);
142+
// Remove cluster manager.
143+
clusterManagers.remove(clusterManager.clusterManagerId);
144+
});
145+
}
146+
147+
void _addMarkersToCluster(ClusterManager clusterManager) {
148+
for (int i = 0; i < _markersToAddToClusterManagerCount; i++) {
149+
final String markerIdVal =
150+
'${clusterManager.clusterManagerId.value}_marker_id_$_markerIdCounter';
151+
_markerIdCounter++;
152+
final MarkerId markerId = MarkerId(markerIdVal);
153+
154+
final int clusterManagerIndex =
155+
clusterManagers.values.toList().indexOf(clusterManager);
156+
157+
// Add additional offset to longitude for each cluster manager to space
158+
// out markers in different cluster managers.
159+
final double clusterManagerLongitudeOffset =
160+
clusterManagerIndex * _clusterManagerLongitudeOffset;
161+
162+
final Marker marker = Marker(
163+
clusterManagerId: clusterManager.clusterManagerId,
164+
markerId: markerId,
165+
position: LatLng(
166+
center.latitude + _getRandomOffset(),
167+
center.longitude + _getRandomOffset() + clusterManagerLongitudeOffset,
168+
),
169+
infoWindow: InfoWindow(title: markerIdVal, snippet: '*'),
170+
onTap: () => _onMarkerTapped(markerId),
171+
);
172+
markers[markerId] = marker;
173+
}
174+
setState(() {});
175+
}
176+
177+
double _getRandomOffset() {
178+
return (Random().nextDouble() - 0.5) * _markerOffsetFactor;
179+
}
180+
181+
void _remove(MarkerId markerId) {
182+
setState(() {
183+
if (markers.containsKey(markerId)) {
184+
markers.remove(markerId);
185+
}
186+
});
187+
}
188+
189+
void _changeMarkersAlpha() {
190+
for (final MarkerId markerId in markers.keys) {
191+
final Marker marker = markers[markerId]!;
192+
final double current = marker.alpha;
193+
markers[markerId] = marker.copyWith(
194+
alphaParam: current == _fullyVisibleAlpha
195+
? _halfVisibleAlpha
196+
: _fullyVisibleAlpha,
197+
);
198+
}
199+
setState(() {});
200+
}
201+
202+
@override
203+
Widget build(BuildContext context) {
204+
final MarkerId? selectedId = selectedMarker;
205+
return Column(
206+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
207+
children: <Widget>[
208+
SizedBox(
209+
height: 300.0,
210+
child: GoogleMap(
211+
onMapCreated: _onMapCreated,
212+
initialCameraPosition: const CameraPosition(
213+
target: LatLng(-33.852, 151.25),
214+
zoom: 11.0,
215+
),
216+
markers: Set<Marker>.of(markers.values),
217+
clusterManagers: Set<ClusterManager>.of(clusterManagers.values),
218+
),
219+
),
220+
Column(children: <Widget>[
221+
Row(
222+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
223+
children: <Widget>[
224+
TextButton(
225+
onPressed: clusterManagers.length >= _clusterManagerMaxCount
226+
? null
227+
: () => _addClusterManager(),
228+
child: const Text('Add cluster manager'),
229+
),
230+
TextButton(
231+
onPressed: clusterManagers.isEmpty
232+
? null
233+
: () => _removeClusterManager(clusterManagers.values.last),
234+
child: const Text('Remove cluster manager'),
235+
),
236+
],
237+
),
238+
Wrap(
239+
alignment: WrapAlignment.spaceEvenly,
240+
children: <Widget>[
241+
for (final MapEntry<ClusterManagerId, ClusterManager> clusterEntry
242+
in clusterManagers.entries)
243+
TextButton(
244+
onPressed: () => _addMarkersToCluster(clusterEntry.value),
245+
child: Text('Add markers to ${clusterEntry.key.value}'),
246+
),
247+
],
248+
),
249+
Wrap(
250+
alignment: WrapAlignment.spaceEvenly,
251+
children: <Widget>[
252+
TextButton(
253+
onPressed: selectedId == null
254+
? null
255+
: () {
256+
_remove(selectedId);
257+
setState(() {
258+
selectedMarker = null;
259+
});
260+
},
261+
child: const Text('Remove selected marker'),
262+
),
263+
TextButton(
264+
onPressed: markers.isEmpty ? null : () => _changeMarkersAlpha(),
265+
child: const Text('Change all markers alpha'),
266+
),
267+
],
268+
),
269+
if (lastCluster != null)
270+
Padding(
271+
padding: const EdgeInsets.all(10),
272+
child: Text(
273+
'Cluster with ${lastCluster!.count} markers clicked at ${lastCluster!.position}')),
274+
]),
275+
],
276+
);
277+
}
278+
}

0 commit comments

Comments
 (0)