Project structure,
- PCL (With Xaml Views)
- .Android project (Targeting android platform)
- .iOS (Targeting iOS platform)
The objective of this project is to use Xamarin.Forms MAP on different platforms with the help of renderer (native code).
- Create a custom Map Control in PCL.
- Now, there is VisibleRegion property for Xamarin.Forms map and we can detect visible region changed in Xaml.CS.
- But, as soon as I added renderer, I got the visible region null. I have added one function in the custom map for visible region changed, in order to get a call back from the renderer.
- On Marker and pin tap, there is no event available at XAML level because we are writing the renderer.
In order to overcome this, we have to add one function in CustomMap (PinClicked).
- public class CustomMap : Map
- {
-
-
- public static readonly BindableProperty RouteCoordinatesProperty =
- BindableProperty.Create("RouteCoordinates", typeof(List<TrackInfo>), typeof(CustomMap), null, BindingMode.TwoWay);
- public List<TrackInfo> RouteCoordinates
- {
- get { return (List<TrackInfo>)GetValue(RouteCoordinatesProperty); }
- set { SetValue(RouteCoordinatesProperty, value); }
- }
-
-
-
- public static readonly BindableProperty CustomPinsProperty =
- BindableProperty.Create("CustomPins", typeof(List<CustomPin>), typeof(CustomMap), null, BindingMode.TwoWay);
-
- public List<CustomPin> CustomPins
- {
- get { return (List<CustomPin>)GetValue(CustomPinsProperty); }
- set
- {
- SetValue(CustomPinsProperty, value);
- }
- }
-
- public Action<SignDetails> PinClicked;
-
- public Func<string,string,Task> OnVisibleRegionChanged;
-
- public CustomMap()
- {
-
- }
-
-
-
- public async Task<GeoPosition> GetGeolocation()
- {
- var result = await App.Container.GetInstance<IGeolocationService>().GetCurrentLocation();
- return result;
- }
- }
This CustomMap in PCL contains several binding properties in order to bind Pins and Tracks to the map from XAML.
CustomMap class uses CustomPin (pin details) and TrackInfo Model,
- public class CustomPin
- {
-
-
-
-
- public Pin pin { get; set; }
- public string pinImage { get; set; }
- public string PinPosition { get; set; }
-
- }
TrackInfo
- public class GeoPosition
- {
- public double Latitude { get; set; }
-
- public double Longitude { get; set; }
- }
-
- public class PositionEstimateList
- {
- public PositionEstimateList()
- {
- actualPosition = new List<GeoPosition>();
- }
- public List<GeoPosition> actualPosition { get; set; }
-
- }
- public class TrackInfo
- {
- public string SubmitterName { get; set; }
- public string trackColor { get; set; }
- public List<PositionEstimateList> PositionEstimateList { get; set; }
- }
Here is the XAML snippet from Home.Xaml (Where I am using my CustomMap),
- <local:CustomMap RouteCoordinates="{Binding RouteCoordinates}"
- CustomPins="{Binding CustomPins}"
- VerticalOptions="FillAndExpand"
- x:Name="TrackingMap"/>
Android renderer
- public class CustomMapRenderer : MapRenderer, IOnMapReadyCallback, IOnMarkerClickListener
- {
- public string TopLeft { get; set; }
- public string BottomRight { get; set; }
- List<Marker> markers;
- GoogleMap map;
- Polyline polyline;
- public double CacheLat { get; set; }
- public double CacheLong { get; set; }
- List<Polyline> PolylineBuffer = new List<Polyline>();
- bool isDrawn;
- public static Random rand = new Random();
- public static Android.Graphics.Color GetRandomColor()
- {
- int hue = rand.Next(255);
- Android.Graphics.Color color = Android.Graphics.Color.HSVToColor(
- new[] {
- hue,
- 1.0f,
- 1.0f,
- }
- );
- return color;
- }
- protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
- {
- base.OnElementChanged(e);
- if (e.OldElement != null)
- {
-
- }
-
- if (e.NewElement != null)
- {
- ((MapView)Control).GetMapAsync(this);
- }
- }
-
-
-
-
-
-
- private async void Map_CameraChange(object sender, CameraChangeEventArgs e)
- {
- map = sender as GoogleMap;
- var zoom = e.Position.Zoom;
- if (((CustomMap)this.Element) != null && map != null)
- {
- var projection = map.Projection.VisibleRegion;
-
- var far_right_lat = Math.Round(projection.FarLeft.Latitude, 2).ToString();
- var far_right_long = Math.Round(projection.FarLeft.Longitude, 2).ToString();
-
- var near_left_lat = Math.Round(projection.NearRight.Latitude, 2).ToString();
- var near_left_long = Math.Round(projection.NearRight.Longitude, 2).ToString();
-
- var centerLatLong = projection.LatLngBounds.Center;
- if (centerLatLong != null)
- {
- App.CurrentLat = centerLatLong.Latitude;
- App.CurrentLong = centerLatLong.Longitude;
- App.CurrentZoomLevel = zoom;
- }
- var near_left = near_left_lat + "," + near_left_long;
- var near_Right = far_right_lat + "," + far_right_long;
- await ((CustomMap)this.Element).OnVisibleRegionChanged(near_left, near_Right);
- }
- }
-
-
-
-
-
-
-
-
-
- protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
- {
- base.OnElementPropertyChanged(sender, e);
- if (this.Element == null || this.Control == null)
- return;
- if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
- {
- var nativeMap = Control as MapView;
-
- if (((CustomMap)this.Element).RouteCoordinates == null)
- {
- RemovePlolyline();
- }
- else
- if (((CustomMap)this.Element).RouteCoordinates.Count == 0)
- {
- RemovePlolyline();
- }
- else
- {
- UpdatePolyLine();
- }
- }
- if (e.PropertyName == CustomMap.CustomPinsProperty.PropertyName)
- {
- var nativeMap = Control as MapView;
- if (markers != null)
- {
- if (markers.Count > 0)
- markers.ForEach(x => x.Remove());
- }
- SetMapMarkers();
- }
- }
- private void SetMapMarkers()
- {
- markers = new List<Marker>();
- if (((CustomMap)this.Element).CustomPins == null)
- return;
- foreach (var custompin in ((CustomMap)this.Element).CustomPins)
- {
- var marker = new MarkerOptions();
- marker.SetPosition(new LatLng(custompin.pin.Position.Latitude, custompin.pin.Position.Longitude));
- marker.SetTitle(custompin.pin.Label);
- marker.SetSnippet(custompin.pin.Address);
- var resource = typeof(Resource.Drawable).GetField(custompin.pinImage);
- int resourceId = 0;
- if (resource != null)
- {
- resourceId = (int)resource.GetValue(custompin.pinImage);
- }
- if (resourceId != 0)
- {
- marker.SetIcon(BitmapDescriptorFactory.FromResource(resourceId));
- map.SetOnMarkerClickListener(this);
- Marker m = map.AddMarker(marker);
- markers.Add(m);
- }
- }
- isDrawn = true;
- }
-
-
-
-
-
-
-
-
-
- public bool OnMarkerClick(Marker marker)
- {
- SignDetails _markerDetails = new SignDetails();
- foreach (var pin in ((CustomMap)this.Element).CustomPins)
- {
- if (pin.pin.Position.Latitude == marker.Position.Latitude)
- {
- if (pin.pin.Position.Longitude == marker.Position.Longitude)
- {
- if (!string.IsNullOrEmpty(pin.pin.Label))
- {
- if (pin.pin.Label == marker.Title)
- {
- _markerDetails.SignLat = pin.pin.Position.Latitude.ToString();
- _markerDetails.SignLong = pin.pin.Position.Longitude.ToString();
- _markerDetails.SignImage = pin.pinImage;
- }
- }
- }
- }
- }
- ((CustomMap)this.Element).PinClicked(_markerDetails);
- return true;
- }
- protected override void OnLayout(bool changed, int l, int t, int r, int b)
- {
- base.OnLayout(changed, l, t, r, b);
- if (changed)
- {
- isDrawn = false;
- }
- }
-
-
-
-
- private void UpdatePolyLine()
- {
- try
- {
- RemovePlolyline();
- foreach (var routeCordinates in ((CustomMap)this.Element).RouteCoordinates)
- {
- foreach (var positionEstimate in routeCordinates.PositionEstimateList)
- {
- if (routeCordinates.SubmitterName == null)
- return;
- var color = routeCordinates.trackColor;
- if (!string.IsNullOrEmpty(color))
- {
- var trackColor = GetColor(color);
- var polylineOptions = new PolylineOptions();
- polylineOptions.InvokeColor(trackColor);
- foreach (var position in positionEstimate.actualPosition)
- {
- polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
- }
- polyline = map.AddPolyline(polylineOptions);
- PolylineBuffer.Add(polyline);
- }
- }
- }
- }
- catch (Exception ex)
- {
- //Log exception to hockey app
- }
- }
-
-
-
-
-
-
-
- private Android.Graphics.Color GetColor(string color)
- {
- Android.Graphics.Color trackColor = Android.Graphics.Color.Orange;
- switch (color)
- {
- case "1":
- trackColor = Android.Graphics.Color.Rgb(77, 123, 224);
- break;
- case "2":
- trackColor = Android.Graphics.Color.Rgb(50, 193, 214);
- break;
- case "3":
- trackColor = Android.Graphics.Color.Rgb(163, 178, 78);
- break;
-
- case "4":
- trackColor = Android.Graphics.Color.Rgb(187, 93, 153);
- break;
- case "5":
- trackColor = Android.Graphics.Color.Rgb(175, 98, 46);
- break;
- }
- return trackColor;
- }
-
-
-
-
- private void RemovePlolyline()
- {
- foreach (Polyline line in PolylineBuffer)
- {
- line.Remove();
- }
- PolylineBuffer.Clear();
- }
- bool _isReady;
- public async void OnMapReady(GoogleMap googleMap)
- {
- try
- {
- if (_isReady) return;
- _isReady = true;
- map = googleMap;
- var bounds = googleMap.Projection.VisibleRegion;
- if (App.CurrentLat == 0 && App.CurrentLong == 0)
- {
- var CurrentPosition = await ((CustomMap)this.Element).GetGeolocation();
- if (CurrentPosition != null)
- {
- App.CurrentLat = CurrentPosition.Latitude;
- App.CurrentLong = CurrentPosition.Longitude;
- map.MoveCamera(CameraUpdateFactory.NewLatLngZoom(new LatLng(App.CurrentLat, App.CurrentLong),
- App.CurrentZoomLevel));
- }
- else
- {
- map.MoveCamera(CameraUpdateFactory.NewLatLngZoom(new LatLng(52.52, 13.40), App.CurrentZoomLevel));
- }
- }
- else
- {
- map.MoveCamera(CameraUpdateFactory.NewLatLngZoom(new LatLng(App.CurrentLat, App.CurrentLong), App.CurrentZoomLevel));
- }
- map.CameraChange += Map_CameraChange;
- await Task.Run(() =>
- {
- if (((CustomMap)this.Element).RouteCoordinates != null)
- {
- if (((CustomMap)this.Element).RouteCoordinates.Count > 0)
- {
- UpdatePolyLine();
- }
- }
- });
- await Task.Run(() =>
- {
- if (((CustomMap)this.Element).CustomPins != null)
- {
- if (((CustomMap)this.Element).CustomPins.Count > 0)
- {
- SetMapMarkers();
- }
- }
- });
- }
- catch (Exception ex)
- {
- //Log exception to hockey app
- }
- }
- }
iOS Renderer [I got the wiered issue that mkMapview automatically resets its location to initial position if we move anywhere in map].
- public class CustomMapRenderer : MapRenderer
- {
- List<string> tempImages = new List<string>();
- public string TopLeft { get; set; }
- public string BottomRight { get; set; }
- public bool IsRegionChange = true;
- List<MKPolyline> _mkPolyLineList = new List<MKPolyline>();
- MKPolylineRenderer polylineRenderer;
- MKPolyline routeOverlay;
- CustomMap map;
- MKMapView nativeMap;
- public CustomMapRenderer()
- {
-
- }
- protected override async void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
- {
- base.OnElementPropertyChanged(sender, e);
- if (nativeMap == null)
- {
- nativeMap = Control as MKMapView;
- nativeMap.ZoomEnabled = true;
- nativeMap.ScrollEnabled = true;
- map = (CustomMap)sender;
-
- }
- if (nativeMap != null)
- if (IsRegionChange)
- {
- await InitializeMap(sender);
- ReDraw();
- IsRegionChange = false;
- }
- if ((this.Element == null) || (this.Control == null))
- return;
-
- if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
- {
- map = (CustomMap)sender;
-
- if (nativeMap == null)
- return;
-
- if (map.RouteCoordinates.Count > 0)
- UpdatePolyLine();
- else
- RemovePolyline();
- }
- if (e.PropertyName == CustomMap.CustomPinsProperty.PropertyName)
- {
- map = (CustomMap)sender;
- map.Pins.Clear();
- if (map.CustomPins != null)
- {
- foreach (var customPin in map.CustomPins)
- {
- map.Pins.Add(customPin.pin);
- }
- }
- InvokePlotPins();
- }
- }
-
- private void RemovePolyline()
- {
- try
- {
- if (_mkPolyLineList != null)
- {
- if (_mkPolyLineList.Count > 0)
- {
- var overlays = nativeMap.Overlays;
- nativeMap.RemoveOverlays(overlays);
- _mkPolyLineList.Clear();
- }
-
-
- }
- }
- catch (Exception ex)
- {
-
- }
- }
-
- private void ReDraw()
- {
- if (map == null)
- return;
- if (map.RouteCoordinates != null)
- if (map.RouteCoordinates.Count > 0)
- UpdatePolyLine();
- if (map.CustomPins != null)
- if (map.CustomPins.Count > 0)
- InvokePlotPins();
- }
-
- private async System.Threading.Tasks.Task InitializeMap(object sender)
- {
- try
- {
- map = (CustomMap)sender;
- nativeMap.RegionChanged += NativeMap_RegionChanged;
-
- if (App.CurrentLat != 0 && App.CurrentLong != 0)
- map.MoveToRegion(new MapSpan(new Position(App.CurrentLat, App.CurrentLong), App.LatitudeDelta, App.LongitudeDelta));
- else
- {
- var location = await map.GetGeolocation();
- if (location != null)
- {
- map.MoveToRegion(new MapSpan(new Position(location.Latitude,
- location.Longitude), 0.01, 0.01));
- }
- else
- {
- map.MoveToRegion(new MapSpan(new Position
- (52.5200, 13.4050), 0.01, 0.01));
- }
- }
- }
- catch (Exception ex)
- {
-
- }
-
- }
-
-
- private async void NativeMap_RegionChanged(object sender, MKMapViewChangeEventArgs e)
- {
- try
- {
- if (Element == null)
- return;
- var position = nativeMap.VisibleMapRect;
- CalculateBoundingCoordinates(nativeMap);
- nativeMap.ZoomEnabled = true;
- nativeMap.ScrollEnabled = true;
- nativeMap.RegionChanged -= NativeMap_RegionChanged;
-
- map.MoveToRegion(new MapSpan(new Position(nativeMap.Region.Center.Latitude, nativeMap.Region.Center.Longitude), nativeMap.Region.Span.LatitudeDelta, nativeMap.Region.Span.LongitudeDelta));
- nativeMap.RegionChanged += NativeMap_RegionChanged;
- if (BottomRight != null && TopLeft != null)
- {
- App.CurrentLat = nativeMap.Region.Center.Latitude;
- App.CurrentLong = nativeMap.Region.Center.Longitude;
- App.LatitudeDelta = nativeMap.Region.Span.LatitudeDelta;
- App.LongitudeDelta = nativeMap.Region.Span.LongitudeDelta;
- await map.OnVisibleRegionChanged(BottomRight, TopLeft);
- }
- }
- catch (Exception ex)
- {
-
- }
- }
-
-
-
-
-
-
- public void CalculateBoundingCoordinates(MKMapView map)
- {
- var center = map.Region.Center;
- var halfheightDegrees = map.Region.Span.LatitudeDelta / 2;
- var halfwidthDegrees = map.Region.Span.LongitudeDelta / 2;
-
-
- var left = Math.Round(center.Longitude - halfwidthDegrees, 2);
- var right = Math.Round(center.Longitude + halfwidthDegrees, 2);
- var top = Math.Round(center.Latitude + halfheightDegrees, 2);
- var bottom = Math.Round(center.Latitude - halfheightDegrees, 2);
-
- if (left < -180) left = 180 + (180 + left);
- if (right > 180) right = (right - 180) - 180;
- TopLeft = top.ToString() + "," + left.ToString();
- BottomRight = bottom.ToString() + "," + right.ToString();
- }
- private void InvokePlotPins()
- {
- if (nativeMap == null)
- return;
-
- nativeMap.GetViewForAnnotation = GetViewForAnnotation;
- }
- private MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
- {
- MKAnnotationView annotationView = null;
- if (annotation is MKUserLocation)
- return null;
- var mkAnnotation = annotation as MKPointAnnotation;
- if (mkAnnotation == null)
- return null;
- var customPin = GetCustomPin(mkAnnotation);
- if (customPin == null)
- {
- return null;
- }
- SignDetails _signDetails = new SignDetails();
- _signDetails.SignLat = customPin.pin.Position.Latitude.ToString();
- _signDetails.SignLong = customPin.pin.Position.Longitude.ToString();
- _signDetails.SignImage = customPin.pinImage;
-
-
- annotationView = new CustomMKAnnotationView(annotation, customPin.pinImage)
- {
- CurrentSignDetails = new SignDetails(_signDetails),
- PinTouch = map.PinClicked
- };
- annotationView.Image = UIImage.FromFile(customPin.pinImage + ".png");
-
-
- return annotationView;
- }
- private CustomPin GetCustomPin(MKPointAnnotation mkAnnotation)
- {
- var position = new Position(mkAnnotation.Coordinate.Latitude, mkAnnotation.Coordinate.Longitude);
- foreach (var pin in map.CustomPins)
- {
- if (pin.pin.Position == position)
- {
- if (!string.IsNullOrEmpty(pin.pin.Label))
- {
- if (pin.pin.Label == mkAnnotation.Title)
- return pin;
- }
- }
- }
- return null;
- }
-
- [Foundation.Export("mapView:rendererForOverlay:")]
- MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlay)
- {
- if (polylineRenderer == null)
- {
- var o = ObjCRuntime.Runtime.GetNSObject(overlay.Handle) as MKPolyline;
- polylineRenderer = new MKPolylineRenderer(o);
- polylineRenderer.FillColor = UIColor.Blue;
- polylineRenderer.StrokeColor = UIColor.Blue;
- polylineRenderer.LineWidth = 2;
- polylineRenderer.Alpha = 0.4f;
- }
- return polylineRenderer;
- }
- private void UpdatePolyLine()
- {
- if (nativeMap == null)
- return;
- try
- {
- RemovePolyline();
- nativeMap.OverlayRenderer = GetOverlayRenderer;
- foreach (var routeCordinates in map.RouteCoordinates)
- {
-
- foreach (var positionEstimate in routeCordinates.PositionEstimateList)
- {
- int index = 0;
- CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[positionEstimate.actualPosition.Count];
- foreach (var position in positionEstimate.actualPosition)
- {
- coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
- index++;
- }
- routeOverlay = MKPolyline.FromCoordinates(coords);
- var o = ObjCRuntime.Runtime.GetNSObject(routeOverlay.Handle) as MKPolyline;
- polylineRenderer = new MKPolylineRenderer(o);
- var trackColor = GetColor(routeCordinates.trackColor);
- polylineRenderer.StrokeColor = trackColor;
- polylineRenderer.LineWidth = 1;
- nativeMap.AddOverlay(routeOverlay);
- _mkPolyLineList.Add(routeOverlay);
- }
- }
- }
- catch (Exception ex)
- {
-
- }
- }
-
- private UIColor GetColor(string color)
- {
- UIColor trackColor = UIColor.Orange;
- switch (color)
- {
- case "1":
- trackColor = UIColor.FromRGB(77, 123, 224);
- break;
- case "2":
- trackColor = UIColor.FromRGB(50, 193, 214);
- break;
- case "3":
- trackColor = UIColor.FromRGB(163, 178, 78);
- break;
-
- case "4":
- trackColor = UIColor.FromRGB(187, 93, 153);
- break;
- case "5":
- trackColor = UIColor.FromRGB(175, 98, 46);
- break;
- }
- return trackColor;
- }
- }
Now, we have everything in place. We can use TrackingMap.PinClicked and TrackingMap.OnVisibleRegionChanged in Xaml.cs to get the visibleRegion changed. And, we can bind our collections from ViewModel to the map so as to show pins and track information. It also allows redrawing the content as the collection changes.