Skip to main content

google_maps_flexible_cluster 0.1.0 copy "google_maps_flexible_cluster: ^0.1.0" to clipboard
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.

example/lib/main.dart

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),
              );
            },
          ),
        ],
      ),
    );
  }
}
1
likes
130
points
15
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A highly customizable clustering solution for Google Maps in Flutter with custom icon painters and efficient performance.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

fluster, flutter, google_maps_flutter

More

Packages that depend on google_maps_flexible_cluster