Xamarin.forms.Maps - Tap to get a position on the map...

Xamarin.Forms.Map component

Xamarin.Forms.Maps is a great component. It gives you the possibility to deal with maps and geolocation problematics in a few lines of code.

This "Map" component internally use native map controls for iOS, Android and Windows Phone:



You can display and locate "Pins" with custom information on the map. I suggest you to read the reference links above.

Display pins with custom information



The problem

But actually, in my mind, the Map control miss a very important feature:
--> Tap  on the map to get the relative location (to put a new pin, to get the relative address...)


The workaround


Use renderers

Actually, to solve this problem, we need to implement custom renderers for iOS and Android. Maybe this feature will be implemented later by Xamarin...

So here is my solution (3 code files below):
- ExtMap.cs: overloaded map control that will contain our "Tapped" event to get the tapped location
- ExtMapRenderer (iOS): renderer for the iOS platform
- ExtMapRenderer (Android): renderer for the android platform

Each renderer will catch and manage the TAP event for us.

ExtMap control


This is the Xamarin forms map control that will override the Xamarin Forms Maps control. We just add the 'Tapped' event and logic to this implementation:

    /// <summary>
    /// Extended map:
    /// allow user to tap a point on the map to get the relative position.
    /// </summary>
    public class ExtMap : Map
    {
        /// <summary>
        /// Event thrown when the user taps on the map
        /// </summary>
        public event EventHandler<MapTapEventArgs> Tapped;

        #region Constructors

        /// <summary>
        /// Default constructor
        /// </summary>
        public ExtMap()
        {

        }

        /// <summary>
        /// Constructor that takes a region
        /// </summary>
        /// <param name="region"></param>
        public ExtMap(MapSpan region)
            : base(region)
        {

        }

        #endregion

        public void OnTap(Position coordinate)
        {
            OnTap(new MapTapEventArgs { Position = coordinate });
        }

        protected virtual void OnTap(MapTapEventArgs e)
        {
            var handler = Tapped;

            if (handler != null)
                handler(this, e);
        }
    }

    /// <summary>
    /// Event args used with maps, when the user tap on it
    /// </summary>
    public class MapTapEventArgs : EventArgs
    {
        public Position Position { get; set; }
    }


iOS Renderer


[assembly: ExportRenderer(typeof(ExtMap), typeof(ExtMapRenderer))]
namespace MyApp.iOS.CustomRenderers
{
    /// <summary>
    /// Renderer for the xamarin ios map control
    /// </summary>
    public class ExtMapRenderer : MapRenderer
    {
        private readonly UITapGestureRecognizer _tapRecogniser;

        public ExtMapRenderer()
        {
            _tapRecogniser = new UITapGestureRecognizer(OnTap)
            {
                NumberOfTapsRequired = 1,
                NumberOfTouchesRequired = 1
            };
        }

        private void OnTap(UITapGestureRecognizer recognizer)
        {
            var cgPoint = recognizer.LocationInView(Control);

            var location = ((MKMapView)Control).ConvertPoint(cgPoint, Control);

            ((ExtMap)Element).OnTap(new Position(location.Latitude, location.Longitude));
        }

        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            if (Control != null)
                Control.RemoveGestureRecognizer(_tapRecogniser);

            base.OnElementChanged(e);

            if (Control != null)
                Control.AddGestureRecognizer(_tapRecogniser);
        }
    }
}


Android Renderer 


[assembly: ExportRenderer(typeof(ExtMap), typeof(ExtMapRenderer))]
namespace MyApp.Android.CustomRenderers
{
    /// <summary>
    /// Renderer for the xamarin map.
    /// Enable user to get a position by taping on the map.
    /// </summary>
    public class ExtMapRenderer : MapRenderer, IOnMapReadyCallback
    {
        // We use a native google map for Android
        private GoogleMap _map;

        public void OnMapReady(GoogleMap googleMap)
        {
            _map = googleMap;

            if (_map != null)
                _map.MapClick += googleMap_MapClick;
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
        {
            if (_map != null)
                _map.MapClick -= googleMap_MapClick;

            base.OnElementChanged(e);

            if (Control != null)
                ((MapView)Control).GetMapAsync(this);
        }

        private void googleMap_MapClick(object sender, GoogleMap.MapClickEventArgs e)
        {
            ((ExtMap)Element).OnTap(new Position(e.Point.Latitude, e.Point.Longitude));
        }
    }
}



So we can now easily pick a location and get the relative address thanks to a geocoding service. You can use Google Apis or use the GeoCoder class provided by Xamarin (see references below):


Tap on the map to get the street address.



References:

Xamarin Forms Map control overview:
http://developer.xamarin.com/guides/cross-platform/xamarin-forms/user-interface/map/

Xamarin Forms Geocoding, GeoCoder class
http://developer.xamarin.com/recipes/cross-platform/xamarin-forms/reverse-geocode/
http://developer.xamarin.com/recipes/cross-platform/xamarin-forms/geocode/



Julien

Some say he’s half man half fish, others say he’s more of a seventy/thirty split. Either way he’s a fishy bastard.

5 commentaires:

  1. Hi. Thank you for this very usefull tutorial. But how about the UWP solution. Do you have any hint for me?
    Thank you!

    RépondreSupprimer
  2. I dont get it. You should have finished the GUI part, that would be great,

    RépondreSupprimer
  3. Android renderer stopped working on Xamarin.Forms 2.3.4

    RépondreSupprimer
  4. I get a exception in ((ExtMap)Element).OnTap(new Position(e.Point.Latitude, e.Point.Longitude)); call System.InvalidCastException: Specified cast is not valid.

    RépondreSupprimer
  5. its great i dont know why they missed this important functionality

    RépondreSupprimer