google_maps_flexible_cluster 0.1.0
google_maps_flexible_cluster: ^0.1.0 copied to clipboard
A highly customizable clustering solution for Google Maps in Flutter with custom icon painters and efficient performance.
import 'package:flutter/material.dart';
import 'package:google_maps_flexible_cluster/google_maps_flexible_cluster.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'
hide ClusterManager;
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Custom Cluster Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const MapScreen(),
);
}
}
// Custom location class extending ClusterableItem
class GymLocation extends ClusterableItem {
final String name;
final String address;
final int gymId;
final int merchantId;
final bool paymentAllowed;
GymLocation({
required super.id,
required super.position,
required this.name,
required this.address,
required this.gymId,
required this.merchantId,
this.paymentAllowed = true,
});
}
// Custom painter that uses an asset image
class AssetClusterPainter extends ClusterIconPainter {
final String clusterAssetPath;
final String itemAssetPath;
AssetClusterPainter({
this.clusterAssetPath = 'assets/images/cluster.png',
this.itemAssetPath = 'assets/images/location-pin-medium.png',
});
@override
Future<Uint8List> paintCluster(ClusterItem cluster) async {
const canvasSize = Size(200, 200);
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
// Load the cluster background image
final ByteData imageData = await rootBundle.load(clusterAssetPath);
final Uint8List imageBytes = imageData.buffer.asUint8List();
final ui.Image image = await decodeImageFromList(imageBytes);
// Scale and draw the image
const double scale = 1.5;
final double scaledWidth = image.width * scale;
final double scaledHeight = image.height * scale;
final Offset imageOffset = Offset(
(canvasSize.width - scaledWidth) / 2,
((canvasSize.height - scaledHeight) / 2) + 8,
);
final Rect imageRect = Rect.fromLTWH(
imageOffset.dx,
imageOffset.dy,
scaledWidth,
scaledHeight,
);
canvas.drawImageRect(
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
imageRect,
Paint()
..filterQuality = ui.FilterQuality.high
..isAntiAlias = true,
);
// Draw the cluster count
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: ClusterUtils.formatClusterSize(cluster.clusterSize),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
final Offset textOffset = Offset(
(canvasSize.width - textPainter.width) / 2,
(canvasSize.height - textPainter.height) / 2,
);
textPainter.paint(canvas, textOffset);
// Generate final image
final ui.Image finalImage = await recorder
.endRecording()
.toImage(canvasSize.width.toInt(), canvasSize.height.toInt());
final ByteData? pngBytes =
await finalImage.toByteData(format: ui.ImageByteFormat.png);
return pngBytes!.buffer.asUint8List();
}
@override
Future<Uint8List> paintItem(ClusterableItem item) async {
// Load the item marker image directly
final ByteData data = await rootBundle.load(itemAssetPath);
return data.buffer.asUint8List();
}
}
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
GoogleMapController? _mapController;
ClusterManager<GymLocation>? _clusterManager;
Set<Marker> _markers = {};
GymLocation? _selectedLocation;
double _currentZoom = 12.0;
// Sample data
final List<GymLocation> _locations = [
GymLocation(
id: '1',
position: const LatLng(41.2995, 69.2401),
name: 'Gym 1',
address: 'Tashkent, Uzbekistan',
gymId: 1,
merchantId: 100,
),
GymLocation(
id: '2',
position: const LatLng(41.3111, 69.2797),
name: 'Gym 2',
address: 'Tashkent, Uzbekistan',
gymId: 2,
merchantId: 101,
),
GymLocation(
id: '3',
position: const LatLng(41.2856, 69.2034),
name: 'Gym 3',
address: 'Tashkent, Uzbekistan',
gymId: 3,
merchantId: 102,
),
// Add more locations as needed
];
@override
void initState() {
super.initState();
_initializeClusterManager();
}
void _initializeClusterManager() {
_clusterManager = ClusterManager<GymLocation>(
items: _locations,
config: const ClusterConfig(
radius: 150,
minZoom: 0,
maxZoom: 19,
minClusterSize: 2,
),
// Use default painter or custom painter
painter: DefaultClusterPainter(
clusterColor: Colors.blue,
itemColor: Colors.red,
),
// Or use asset-based painter:
// painter: AssetClusterPainter(),
);
_updateMarkers();
}
Future<void> _updateMarkers() async {
if (_clusterManager == null) return;
final markers = await _clusterManager!.getMarkers(
zoom: _currentZoom,
onMarkerTap: (id, item) {
setState(() {
_selectedLocation = item;
});
_showLocationDetails(item);
},
onClusterTap: (id, cluster) {
_zoomToCluster(cluster);
},
);
setState(() {
_markers = markers;
});
// Print stats for debugging
final stats = _clusterManager!.getStats();
debugPrint('Clusters: ${stats['clusters']}, Items: ${stats['items']}');
}
void _showLocationDetails(GymLocation location) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
location.name,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(location.address),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// Navigate to details or perform action
},
child: const Text('View Details'),
),
],
),
),
);
}
Future<void> _zoomToCluster(ClusterItem cluster) async {
if (_mapController == null) return;
await _mapController!.animateCamera(
CameraUpdate.newLatLngZoom(
cluster.position,
_currentZoom + 2,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Cluster Example'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
_clusterManager?.invalidate();
_updateMarkers();
},
),
],
),
body: GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(41.2995, 69.2401),
zoom: 12,
),
markers: _markers,
onMapCreated: (controller) {
_mapController = controller;
},
onCameraMove: (position) {
_currentZoom = position.zoom;
},
onCameraIdle: () {
_updateMarkers();
},
myLocationEnabled: true,
myLocationButtonEnabled: false,
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'zoom_in',
mini: true,
child: const Icon(Icons.add),
onPressed: () async {
final zoom = await _mapController?.getZoomLevel() ?? 12;
_mapController?.animateCamera(
CameraUpdate.zoomTo(zoom + 1),
);
},
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'zoom_out',
mini: true,
child: const Icon(Icons.remove),
onPressed: () async {
final zoom = await _mapController?.getZoomLevel() ?? 12;
_mapController?.animateCamera(
CameraUpdate.zoomTo(zoom - 1),
);
},
),
],
),
);
}
}