diff --git a/example/lib/main.dart b/example/lib/main.dart index 3c0e84a..f8d61d7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:apple_maps_flutter_example/map_update.dart'; import 'package:apple_maps_flutter_example/padding.dart'; import 'package:apple_maps_flutter_example/place_annotation.dart'; +import 'package:apple_maps_flutter_example/place_annotation_clustered.dart'; import 'package:apple_maps_flutter_example/place_circle.dart'; import 'package:apple_maps_flutter_example/place_polyline.dart'; import 'package:apple_maps_flutter_example/place_polygon.dart'; @@ -26,6 +27,7 @@ final List _allPages = [ MoveCameraPage(), PaddingPage(), PlaceAnnotationPage(), + PlaceAnnotationClusteredPage(), AnnotationIconsPage(), PlacePolylinePage(), PlacePolygonPage(), diff --git a/example/lib/place_annotation_clustered.dart b/example/lib/place_annotation_clustered.dart new file mode 100644 index 0000000..ae8ce2e --- /dev/null +++ b/example/lib/place_annotation_clustered.dart @@ -0,0 +1,166 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:apple_maps_flutter/apple_maps_flutter.dart'; +import 'package:flutter/services.dart'; + +import 'page.dart'; +import 'dart:ui' as ui; + +class PlaceAnnotationClusteredPage extends ExamplePage { + PlaceAnnotationClusteredPage() + : super(const Icon(Icons.place), 'Place annotation clustered'); + + @override + Widget build(BuildContext context) { + return const PlaceAnnotationClusteredBody(); + } +} + +class PlaceAnnotationClusteredBody extends StatefulWidget { + const PlaceAnnotationClusteredBody(); + + @override + State createState() => PlaceAnnotationClusteredBodyState(); +} + +typedef Annotation AnnotationUpdateAction(Annotation annotation); + +class PlaceAnnotationClusteredBodyState + extends State { + PlaceAnnotationClusteredBodyState(); + static final LatLng center = const LatLng(-33.86711, 151.1947171); + + AppleMapController controller; + Map annotations = {}; + AnnotationId selectedAnnotation; + int _annotationIdCounter = 1; + BitmapDescriptor _annotationIcon; + BitmapDescriptor _iconFromBytes; + double _devicePixelRatio = 3.0; + + void _onMapCreated(AppleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onAnnotationTapped(AnnotationId annotationId) { + final Annotation tappedAnnotation = annotations[annotationId]; + if (tappedAnnotation != null) { + setState(() { + if (annotations.containsKey(selectedAnnotation)) { + final Annotation resetOld = + annotations[selectedAnnotation].copyWith(); + annotations[selectedAnnotation] = resetOld; + } + selectedAnnotation = annotationId; + }); + } + } + + void _add(String iconType) { + final int annotationCount = annotations.length; + + if (annotationCount == 12) { + return; + } + + final String annotationIdVal = 'annotation_id_$_annotationIdCounter'; + _annotationIdCounter++; + final AnnotationId annotationId = AnnotationId(annotationIdVal); + + final Annotation annotation = Annotation( + annotationId: annotationId, + icon: iconType == 'marker' + ? BitmapDescriptor.markerAnnotation + : iconType == 'pin' + ? BitmapDescriptor.defaultAnnotation + : iconType == 'customAnnotationFromBytes' + ? _iconFromBytes + : _annotationIcon, + position: LatLng( + center.latitude + sin(_annotationIdCounter * pi / 6.0) / 20.0, + center.longitude + cos(_annotationIdCounter * pi / 6.0) / 20.0, + ), + infoWindow: InfoWindow( + title: annotationIdVal, + anchor: Offset(0.5, 0.0), + snippet: '*', + onTap: () => print('InfowWindow of id: $annotationId tapped.')), + onTap: () { + _onAnnotationTapped(annotationId); + }, + ); + + setState(() { + annotations[annotationId] = annotation; + }); + } + + Future _createAnnotationImageFromAsset( + BuildContext context, double devicelPixelRatio) async { + if (_annotationIcon == null) { + final ImageConfiguration imageConfiguration = + ImageConfiguration(devicePixelRatio: devicelPixelRatio); + BitmapDescriptor.fromAssetImage( + imageConfiguration, 'assets/red_square.png') + .then(_updateBitmap); + } + } + + void _updateBitmap(BitmapDescriptor bitmap) { + setState(() { + _annotationIcon = bitmap; + }); + } + + Future _getBytesFromAsset(String path, int width) async { + ByteData data = await rootBundle.load(path); + ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), + targetWidth: width); + ui.FrameInfo fi = await codec.getNextFrame(); + _iconFromBytes = BitmapDescriptor.fromBytes( + (await fi.image.toByteData(format: ui.ImageByteFormat.png)) + .buffer + .asUint8List()); + } + + @override + Widget build(BuildContext context) { + _createAnnotationImageFromAsset(context, _devicePixelRatio); + _getBytesFromAsset('assets/red_square.png', 40); + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + child: AppleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11, + ), + annotations: Set.of(annotations.values), + enableClustering: true, + ), + ), + ), + FlatButton( + child: const Text('customAnnotation from bytes'), + onPressed: () => _add('customAnnotationFromBytes'), + ), + ]); + } +} diff --git a/ios/Classes/Annotations/ClusterableAnnotationView.swift b/ios/Classes/Annotations/ClusterableAnnotationView.swift new file mode 100644 index 0000000..ccb59c8 --- /dev/null +++ b/ios/Classes/Annotations/ClusterableAnnotationView.swift @@ -0,0 +1,34 @@ +// +// ClusteredAnnotationView.swift +// apple_maps_flutter +// +// Created by sarupu on 15.02.2021. +// + +import MapKit + +@available(iOS 11.0, *) +class ClusterableAnnotationView: MKAnnotationView { + + var lastAnnotation: FlutterAnnotation? + + override var annotation: MKAnnotation? { + didSet { + guard let mapItem = annotation as? FlutterAnnotation, mapItem != lastAnnotation else { return } + clusteringIdentifier = "apple_maps_flutter_ci" + image = mapItem.icon.image + lastAnnotation = mapItem + } + } +} + +@available(iOS 11.0, *) +final class ClusterAnnotationView: MKAnnotationView { + override var annotation: MKAnnotation? { + didSet { + guard let cluster = annotation as? MKClusterAnnotation, let firstAnnotation = cluster.memberAnnotations.first as? FlutterAnnotation else { return } + displayPriority = .defaultHigh + image = firstAnnotation.image + } + } +} diff --git a/ios/Classes/MapView/AppleMapController.swift b/ios/Classes/MapView/AppleMapController.swift index 7f3b3ad..52f6be3 100644 --- a/ios/Classes/MapView/AppleMapController.swift +++ b/ios/Classes/MapView/AppleMapController.swift @@ -29,7 +29,6 @@ public class AppleMapViewFactory: NSObject, FlutterPlatformViewFactory { } } - public class AppleMapController : NSObject, FlutterPlatformView, MKMapViewDelegate { var mapView: FlutterMapView! var registrar: FlutterPluginRegistrar @@ -43,6 +42,8 @@ public class AppleMapController : NSObject, FlutterPlatformView, MKMapViewDelega var onCalloutTapGestureRecognizer: UITapGestureRecognizer? var currentlySelectedAnnotation: String? + var isClusteringEnabled = false + public init(withFrame frame: CGRect, withRegistrar registrar: FlutterPluginRegistrar, withargs args: Dictionary ,withId id: Int64) { self.options = args["options"] as! [String: Any] self.channel = FlutterMethodChannel(name: "apple_maps_plugin.luisthein.de/apple_maps_\(id)", binaryMessenger: registrar.messenger()) @@ -55,10 +56,22 @@ public class AppleMapController : NSObject, FlutterPlatformView, MKMapViewDelega self.polygonController = PolygonController(mapView: mapView, channel: channel, registrar: registrar) self.circleController = CircleController(mapView: mapView, channel: channel, registrar: registrar) self.initialCameraPosition = args["initialCameraPosition"]! as! Dictionary - + self.isClusteringEnabled = args["clusteringEnabled"] as! Bool super.init() self.mapView.delegate = self + + if isClusteringEnabled { + if #available(iOS 11.0, *) { + mapView.register( + ClusterableAnnotationView.self, + forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier) + mapView.register( + ClusterAnnotationView.self, + forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier) + } + } + self.mapView.setCenterCoordinate(initialCameraPosition, animated: false) self.setMethodCallHandlers() @@ -93,13 +106,26 @@ public class AppleMapController : NSObject, FlutterPlatformView, MKMapViewDelega } public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { - if let annotation :FlutterAnnotation = view.annotation as? FlutterAnnotation { - if annotation.infoWindowConsumesTapEvents { - view.addGestureRecognizer(self.onCalloutTapGestureRecognizer!) + let annotationCandidate: MKAnnotation? + print("didSelectView called") + + if #available(iOS 11.0, *) { + if let cluster = view.annotation as? MKClusterAnnotation { + annotationCandidate = cluster.memberAnnotations.first + } else { + annotationCandidate = view.annotation } - self.currentlySelectedAnnotation = annotation.id - self.annotationController.onAnnotationClick(annotation: annotation) + } else { + annotationCandidate = view.annotation } + + guard let annotation :FlutterAnnotation = annotationCandidate as? FlutterAnnotation else { return } + + if annotation.infoWindowConsumesTapEvents { + view.addGestureRecognizer(self.onCalloutTapGestureRecognizer!) + } + self.currentlySelectedAnnotation = annotation.id + self.annotationController.onAnnotationClick(annotation: annotation) } public func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { @@ -109,6 +135,9 @@ public class AppleMapController : NSObject, FlutterPlatformView, MKMapViewDelega public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + guard !isClusteringEnabled else { + return nil + } if annotation is MKUserLocation { return nil } else if let flutterAnnotation = annotation as? FlutterAnnotation { diff --git a/lib/src/apple_map.dart b/lib/src/apple_map.dart index 5f483f6..601fc02 100644 --- a/lib/src/apple_map.dart +++ b/lib/src/apple_map.dart @@ -41,6 +41,7 @@ class AppleMap extends StatefulWidget { this.onCameraIdle, this.onTap, this.onLongPress, + this.enableClustering = false, }) : assert(initialCameraPosition != null), super(key: key); @@ -163,6 +164,11 @@ class AppleMap extends StatefulWidget { /// native controls. final EdgeInsets padding; + /// Enables or disables MapKit native clustering. + /// + /// Warning: Experimental. This feature has only been tested with custom icon annotations. + final bool enableClustering; + @override State createState() => _AppleMapState(); } @@ -186,6 +192,7 @@ class _AppleMapState extends State { 'polylinesToAdd': _serializePolylineSet(widget.polylines), 'polygonsToAdd': _serializePolygonSet(widget.polygons), 'circlesToAdd': _serializeCircleSet(widget.circles), + 'clusteringEnabled': widget.enableClustering, }; if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView(