Recently I have had to navigate through Xamarin.Forms Maps for a project. After some documentation reading I found that XF Maps does not offer a lot of the customisation and functionality of the native map libraries (MapKit and Google Maps). This left me having to write some custom renderers to complete the project’s requirements.
In this Xamarin how-to post, I will show you how to obtain the latitude, longitude and radius from the center of the map every time the user moves or zooms. This is especially useful if you have dynamically updating pins. Instead of bloating the map with 1000’s of pins, you will be able to show pins in the area of the map that is visible by the user.
Before getting busy with the custom renderers you must initialize and configure Xamarin.Forms Maps. I highly recommend reading though the offical documentation here.
Shared Code
By inheriting from XFMaps we can create a custom event that will trigger every time the user moves the map and retrieve some custom arguments (latitude, longitude and radius).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
using System; using Xamarin.Forms; using Xamarin.Forms.Maps; namespace XhtCustomMap.Controls { public class CustomMap : Map { public CustomMap(MapSpan region) { MoveToRegion(region); } public event EventHandler<MapEventArgs> MapMoved; public void OnMapMoved(MapEventArgs e) { MapMoved?.Invoke(this, e); } } public class MapEventArgs : EventArgs { public MapEventArgs(double latitude, double longitude, double radius) { Latitude = latitude; Longitude = longitude; Radius = radius; } public double Latitude { private set; get; } public double Longitude { private set; get; } public double Radius { private set; get; } } } |
iOS Custom Renderer (MapKit)
Here we will be subscribing to the RegionChanged event of the MKMapView to retrieve the updated coordinates and radius of the user and sending the information to our custom event that we created earlier. To calculate the radius from the current zoom level of the user we get the distance of the total visible longitude and latitude and determine which one is greater. We use the greater value in case the map is not square and are then still able to utilise the full view of the map.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
using System; using CoreLocation; using DarkIce.Toolkit.Core.Utilities; using MapKit; using Xamarin.Forms; using Xamarin.Forms.Maps.iOS; using Xamarin.Forms.Platform.iOS; using XhtCustomMap.Controls; using XhtCustomMap.Renderers.iOS; [assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer_iOS))] namespace XhtCustomMap.Renderers.iOS { public class CustomMapRenderer_iOS : MapRenderer { private MKMapView _nativeMap; private CustomMap _formsMap; protected override void OnElementChanged(ElementChangedEventArgs<View> e) { base.OnElementChanged(e); _nativeMap = Control as MKMapView; if (_nativeMap == null) { _nativeMap = new MKMapView(); SetNativeControl(_nativeMap); } if (e.OldElement != null) { _nativeMap.RegionChanged -= RegionChanged; } if (e.NewElement != null) { _formsMap = (CustomMap)e.NewElement; _nativeMap.RegionChanged += RegionChanged; } } private void RegionChanged(object sender, MKMapViewChangeEventArgs e) { _formsMap.OnMapMoved(new MapEventArgs(_nativeMap.CenterCoordinate.Latitude, _nativeMap.CenterCoordinate.Longitude, GetRadius())); $"Latitude: {_nativeMap.CenterCoordinate.Latitude}, Longitude: {_nativeMap.CenterCoordinate.Longitude}, Radius: {GetRadius()}km".Log(); } private double GetRadius() { var span = _nativeMap.Region.Span; var center = _nativeMap.Region.Center; var loc1 = new CLLocation(latitude: center.Latitude - span.LatitudeDelta * 0.5, longitude: center.Longitude); var loc2 = new CLLocation(latitude: center.Latitude + span.LatitudeDelta * 0.5, longitude: center.Longitude); var loc3 = new CLLocation(latitude: center.Latitude, longitude: center.Longitude - span.LongitudeDelta * 0.5); var loc4 = new CLLocation(latitude: center.Latitude, longitude: center.Longitude + span.LongitudeDelta * 0.5); var metresInLatitude = loc1.DistanceFrom(loc2); var metresInLongitude = loc3.DistanceFrom(loc4); var diameter = Math.Min(metresInLatitude, metresInLongitude); return diameter / 1000.0 / 2.0; } } } |
Android Custom Renderer (GoogleMap)
The Android custom renderer is very similar to the iOS one, however we use the event CameraIdle of GoogleMaps and must wait to the map is ready before subscribing to the event. This event will trigger after the camera has finished moving. The radius is calculated with a similar method as iOS for consistency between platforms.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
using System; using Android.Content; using Android.Gms.Maps; using Android.Locations; using DarkIce.Toolkit.Core.Utilities; using Xamarin.Forms; using Xamarin.Forms.Maps; using Xamarin.Forms.Maps.Android; using Xamarin.Forms.Platform.Android; using XhtCustomMap.Controls; using XhtCustomMap.Droid.Renderers; [assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer_Android))] namespace XhtCustomMap.Droid.Renderers { public class CustomMapRenderer_Android : MapRenderer { private CustomMap _formsMap; private Context _localContext; public CustomMapRenderer_Android(Context context) : base(context) { _localContext = context; } protected override void OnElementChanged(ElementChangedEventArgs<Map> e) { base.OnElementChanged(e); if (e.OldElement != null) { NativeMap.CameraIdle -= CameraIdle; } if (e.NewElement != null) { _formsMap = (CustomMap)e.NewElement; } } protected override void OnMapReady(GoogleMap map) { base.OnMapReady(map); NativeMap.CameraIdle += CameraIdle; } private void CameraIdle(object sender, EventArgs e) { _formsMap.OnMapMoved(new MapEventArgs(NativeMap.CameraPosition.Target.Latitude, NativeMap.CameraPosition.Target.Longitude, GetRadius())); $"Latitude: {NativeMap.CameraPosition.Target.Latitude}, Longitude: {NativeMap.CameraPosition.Target.Longitude}, Radius: {GetRadius()}km".Log(); } private double GetRadius() { var visibleRegion = NativeMap.Projection.VisibleRegion; var farRight = visibleRegion.FarRight; var farLeft = visibleRegion.FarLeft; var nearRight = visibleRegion.NearRight; var nearLeft = visibleRegion.NearLeft; float[] distanceWidth = new float[2]; Location.DistanceBetween( (farRight.Latitude + nearRight.Latitude) / 2, (farRight.Longitude + nearRight.Longitude) / 2, (farLeft.Latitude + nearLeft.Latitude) / 2, (farLeft.Longitude + nearLeft.Longitude) / 2, distanceWidth ); float[] distanceHeight = new float[2]; Location.DistanceBetween( (farRight.Latitude + nearRight.Latitude) / 2, (farRight.Longitude + nearRight.Longitude) / 2, (farLeft.Latitude + nearLeft.Latitude) / 2, (farLeft.Longitude + nearLeft.Longitude) / 2, distanceHeight ); if (distanceWidth[0] > distanceHeight[0]) { return distanceWidth[0] / 1000.0 / 2.0; } else { return distanceHeight[0] / 1000.0 / 2.0; } } } } |
Using the Custom Control
The custom map can be set in initialised in xaml the same as the original XFMaps package. In the code behind we can subscribe to our custom event and perform any updates based on the users new location.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:XhtCustomMap.Controls" xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps" x:Class="XhtCustomMap.MainPage"> <Grid RowDefinitions="0.7*,0.3*"> <controls:CustomMap x:Name="MyMap"> <x:Arguments> <maps:MapSpan> <x:Arguments> <maps:Position> <x:Arguments> <x:Double>-25.734968</x:Double> <x:Double>134.489563</x:Double> </x:Arguments> </maps:Position> <x:Double>40</x:Double> <x:Double>36</x:Double> </x:Arguments> </maps:MapSpan> </x:Arguments> </controls:CustomMap> <StackLayout Margin="30" Grid.Row="1"> <Label x:Name="Latitude"/> <Label x:Name="Longitude"/> <Label x:Name="Radius"/> </StackLayout> </Grid> </ContentPage> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using System; using System.Collections.Generic; using Xamarin.Forms; using XhtCustomMap.Controls; namespace XhtCustomMap { public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); MyMap.MapMoved += UpdateCoordinates; } ~MainPage() { MyMap.MapMoved -= UpdateCoordinates; } private void UpdateCoordinates(object sender, MapEventArgs e) { Latitude.Text = $"Latitude: {e.Latitude}"; Longitude.Text = $"Longitude: {e.Longitude}"; Radius.Text = $"Radius: {e.Radius}km"; } } } |
I hope this has helped everyone that needs this specific functionality. Please don’t hesitate contact or comment below with any questions.
The full source code is available here.
Leave A Comment