Customising MyLocationOverlay in Xamarin.Android

Introduction

Android has available, a very easy mechanism for visualising the user’s current location in Google Maps - MyLocationOverlay. With some extension, this Activity implementation can become a very versatile addition to your toolkit.

I’m not going to go over the process getting Google Maps running in your project - the online tutorials cover this perfectly well. What I will demonstrate is:

Broadcasting the Current Location

Android has mechanisms for achieving this kind of functionality - Broadcasts and Broadcast Receivers, but there’s a bit of ceremony in setting those up. For the sake of simplicity in this example I’m going to demonstrate a nifty library called TinyMessenger, along with my favourite little IoC container, TinyIoC. They’re available via NuGet and are each added to your project via a single .cs file.

The first thing we need to do after including TinyIoC and TinyMessenger in our project is comment out line 19 in TinyIoC. This will include a preprocessor directive that causes TinyIoC to register an instance of TinyMessengerHub with the default container. After this we can just include a parameter implementing ITinyMessengerHub in our constructors and have the container resolve it for us. We’ll see this further down.

Next we need to define the message type that will be sent with location updates. TinyMessenger facilitates this nicely with generics - we’ll use Android’s Location type.

using Android.Locations;
using TinyMessenger;

namespace Manadart
{
    public class LocationMsg : GenericTinyMessage<Location>
    {
        public LocationMsg(object sender, Location content) : base(sender, content) { }
    }
}

To finish our basic implementation, we inherit from MyLocationOverlay, ensuring that our constructor takes a parameter implementing ITinyMessengerHub. Then we override OnLocationChanged() to publish the location asynchronously via the hub.

using Android.Content;
using Android.GoogleMaps;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Locations;
using TinyMessenger;

namespace Manadart
{
    public class CustomLocationOverlay : MyLocationOverlay
    {
        private readonly ITinyMessengerHub _messengerHub;

        public LocationOverlay(Context context, MapView mapView, ITinyMessengerHub messengerHub) : base(context, mapView)
        {
            _messengerHub = messengerHub;   
        }

        public override void OnLocationChanged(Location location)
        {
            base.OnLocationChanged(location);
            _messengerHub.PublishAsync(new LocationMsg(this, location));
        }
    }
}

Now we’re cooking. All we have to do in some other part of our application is subscribe to messages of the type we’ve defined and do something with them. A contrived listener might look something like this.

using Android.Util;
using TinyMessenger;

namespace Manadart
{
    public class LocationListener
    {
        private readonly ITinyMessengerHub _messengerHub;
        private TinyMessageSubscriptionToken _locationToken;

        public LocationListener(ITinyMessengerHub messengerHub)
        {
            _messengerHub = messengerHub;
            Subscribe();
        }

        private void Subscribe()
        {
            _locationToken = _messengerHub.Subscribe<LocationMsg>(HandleLocationMessage);
        }

        private void HandleLocationMessage(LocationMsg message)
        {
            var content = message.Content;
            Log.Info(
                "CustomLocationOverlay",
                string.Format("I received coordinates {0}, {1} from my custom location overlay", content.Latitude, content.Longitude));
        }

        // Either call this explicitly, or in a Dispose() implementation.
        public void Unsubscribe()
        {
            if (_locationToken != null) _messengerHub.Unsubscribe<LocationMsg>(_locationToken);
        }
    }
}

Distance Threshold

This is quite trivial, but I’ll include it for completeness. All we’re doing is setting a minimum value in meters that the location must change by in order to cause a location broadcast. Each time our threshold is exceeded, we store the location to compare any newly reported locations against. This way, we avoid sending and processing messages for trivial movements. This is how our overlay class should look now.

using Android.Content;
using Android.GoogleMaps;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Locations;
using TinyMessenger;

namespace Manadart
{
    public class CustomLocationOverlay : MyLocationOverlay
    {
        private readonly ITinyMessengerHub _messengerHub;
        private Location _lastLocation;

        public int DistanceThreshold { get; set; }

        public LocationOverlay(Context context, MapView mapView, ITinyMessengerHub messengerHub) : base(context, mapView)
        {
            _messengerHub = messengerHub;
        }

        public override void OnLocationChanged(Location location)
        {
            base.OnLocationChanged(location);
            if (!ThresholdExceeded(location)) return;
            _lastLocation = location;
            _messengerHub.PublishAsync(new LocationMsg(this, location));
        }

        private bool ThresholdExceeded(Location location)
        {
            if (DistanceThreshold < 1 || _lastLocation == null) return true;
            return location.DistanceTo(_lastLocation) >= DistanceThreshold;
        }
    }
}

Using a Custom Location Icon

Using your own icon with MyLocationOverlay isn’t hard, but needs to be done a partcular way if you want to keep functionality like the compass and the icon shadow. The example here behaves gracefully if no custom image has been set for the overlay.

First of all, we need to add a property to our overlay for the image. This is going to be of type Drawable. Note that we have to set bounds on the object, so the map knows how to draw it relative to a map point - in this case, the centre/bottom of the image.

Next we have to override the Draw() method. This knocks out some functionality that would otherwise be handled for us in the default implementation, so we’ll have to take up the slack. The new method below draws using the default technique if no custom image has been provided by calling through to DrawMyLocation(). It also explicitly draws the compass if it is enabled.

So here we have the final version of our customised implementation of MyLocationOverlay.

using Android.Content;
using Android.GoogleMaps;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Locations;
using TinyMessenger;

namespace Manadart
{
    public class CustomLocationOverlay : MyLocationOverlay
    {
        private readonly ITinyMessengerHub _messengerHub;
        private Location _lastLocation;

        public int DistanceThreshold { get; set; }

        public Drawable MarkerResource
        {
            set
            {
                _locationMarker = value;

                // Anchor is the bottom/centre of the image.
                var widthOffset = _locationMarker.IntrinsicWidth / 2;
                _locationMarker.SetBounds(-widthOffset, -_locationMarker.IntrinsicHeight, widthOffset, 0);
            }
        }

        public LocationOverlay(Context context, MapView mapView, ITinyMessengerHub messengerHub) : base(context, mapView)
        {
            _messengerHub = messengerHub;
        }

        public override void OnLocationChanged(Location location)
        {
            base.OnLocationChanged(location);
            if (!ThresholdExceeded(location)) return;
            _lastLocation = location;
            _messengerHub.PublishAsync(new LocationMsg(this, location));
        }

        public override bool Draw(Canvas canvas, MapView mapView, bool shadow, long when)
        {
            if (LastFix == null) return false;

            if (_locationMarker != null)
            {
                var screenPoint = new Point();
                var geoPoint = new GeoPoint((int)(LastFix.Latitude * 1E6), (int)(LastFix.Longitude * 1E6));
                mapView.Projection.ToPixels(geoPoint, screenPoint);
                DrawAt(canvas, _locationMarker, screenPoint.X, screenPoint.Y, shadow);
            }
            else if (MyLocation != null) DrawMyLocation(canvas, mapView, LastFix, MyLocation, when);

            if (IsCompassEnabled) DrawCompass(canvas, Orientation);
            return false;
        }

        private bool ThresholdExceeded(Location location)
        {
            if (DistanceThreshold < 1 || _lastLocation == null) return true;
            return location.DistanceTo(_lastLocation) >= DistanceThreshold;
        }
    }
}

Now we have an overlay that’s highly re-usable. It’s easily themed based on the particular application we want to use it with; and we can utilise the location being reported to our overlay in a myriad of ways without tight coupling or further dependencies.

Happy Hacking.