Custom Map Renderer on Android: Part 2

In the previous post, we setup our DevDaysSpeakers app to include a page that displays a map. To get the map to display our information, we have a few things we have to do. Let’s start with subclassing the Xamarin Forms Map class with our own class and create our own pin data object.


public class CustomPin
{
    public Pin Pin{ get; set; }
    public int Id { get; set; }
    public string Url { get; set; }
}

public class CustomMap : Map
{
    public List<CustomPin> CustomPins { get; set; }
}

And now we will use our new CustomMap class in our XAML. Make sure that you have the namespace declared (see view namespace below) and then you declare it as below. Note that I am just using some of the properties that are declared in the base Map class.


<?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:view="clr-namespace:DevDaysSpeakers.View"
    xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
    prism:ViewModelLocator.AutowireViewModel="True"
    xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
    Title="Map"
    x:Class="DevDaysSpeakers.View.MapPage">
    <Grid>
        <!-- lets reference our custom map now -->
        <view:CustomMap x:Name="cusmap"
            IsShowingUser="True"
            MapType="Street" />
    </Grid>
</ContentPage>

So if we were to run the app at this point, we would see the exact same thing as before. To add the custom behavior, we need to add in our custom map renderer in the Android project. So let’s get started.

In our droid project, let’s create a new class called CustomMapRenderer in the root of the project. The very first thing that we see is that we have to decorate the code at the namespace level to tell Xamarin that whenever we see the CustomMap class in our shared XAML, we should implement using this class. It will look like the following:


/// usings are here

[assembly: ExportRenderer(typeof(DevDaySpeakers.View.CustomMap), typeof(DevDaySpeakers.Droid.CustomMapRenderer)]
namespace DevDaySpeakers.Droid
{
    public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
    {
        /// ... implementation
    }
}

Included above is the class declaration and you can see that it derives from MapRenderer class and implements a pair of interfaces that are used to display the map in Android.

In the CustomMapRenderer, we want to keep track of a couple of instances, the first being our subclasssed Map (CustomMap) and the actual GoogleMap object is in Android. These objects are picked up in a couple of different places.

First, the CustomMap object is captured when the CustomMapRenderer instance is assigned and you can capture that by overriding OnElementChanged. You also use this method to clean up your wired up event handler for when a user taps on the info window associated with a map marker.

Secondly, in our implementation of the IOnMapReadyCallback interface, our implementation of the OnMapReady function saves the instance of the GoogleMap.


/// usings are here

[assembly: ExportRenderer(typeof(View.CustomMap), typeof(Droid.CustomMapRenderer)]
namespace DevDaySpeakers.Droid
{
    public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
    {
        private GoogleMap _map = null;
        private DevDaySpeakers.View.CustomMap _customMap = null;

        protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
        {
            base.OnElementChanged(e);
            if (e.OldElement != null)
            {
                // remove the event handler for the info window tap
                _map.InfoWindowClick -= OnMapInfoWindowClick;
            }

            if (e.NewElement != null)
            {
                // save reference to the CustomMap that was
                // in the XAML declaration
                _customMap  = (View.CustomMap) e.NewElement;
                ((MapView) Control).GetMapAsync(this);
            }
        }

        public void OnMapReady(GoogleMap googleMap)
        {
            /// called when the GoogleMap is ready, we need to save the
            /// instance, wire up the event handler for the info window tap,
            /// tell the map to use this class for handling the info window
            /// and finally load the markers.
            _map = googleMap;
            _map.InfoWindowClick += OnMapInfoWindowClick;
            _map.SetInfoWindowAdapter(this);

            /// load the pins
            UpdatePins();
        }

        private void OnMapInfoWindowClick(object s, GoogleMap.InfoWindowClickEventArgs e)
        {
            // event handler stub
        }

        public Android.Views.View GetInfoContents(Marker marker)
        {
            /// for GoogleMap.IInfoWindowAdapter implementation
            /// will just do default
            return null;
        }

        public Android.Views.View GetInfoWindow(Marker marker)
        {
            /// for GoogleMap.IInfoWindowAdapter implementation
            /// will just do default
            return null;
        }

        private void UpdatePins()
        {
            /// stub for updating pins on the map
        }
    }
}

Most of that is pretty straight forward I think. We are implementing our interfaces and saving instances to the Map objects so that we can use them later. We are also wiring up the event handler so that can capture when the user taps the information window that appears after tapping on a map marker.

Next we will handle adding the pins to the map by implementing the UpdatePins method. The interesting thing in this is that our CustomMap instance (_customMap) contains the business data of the app, while the GoogleMap (_map) instance does all the presentation.


private void UpdatePins()
{
    if (_map == null)
        return;

    _map.Clear();
    foreach (var pin in _customMap.CustomPins)
    {
        var marker = new MarkerOptions();
        marker.SetPosition(
            new LatLng(pin.Pin.Position.Latitude,
            pin.Pin.Position.Longitude));
        marker.SetTitle(pin.Pin.Label);
        marker.SetAddress(pin.Pin.Address);
        marker.SetIcon(BitmapDescriptorFactory.DefaultMarker(210));
        _map.AddMarker(marker);
    }
}

From above you can see that we are creating markers, setting the title property and the address property, and also changing the look of the pin itself. In this case we are only changing the color from default to blue. But you can also add in your own bitmaps if you want.

Let’s talk a bit about customizing the info window. In case you aren’t sure about the info window, this is what is pop’d up on the map when you tap on the marker. We have the ability to customize it and this happens in the GetInfoContents method in the code fragment above. By returning null, you will just get the default contents.

If you want to do something custom, you will need to define your own layout in the Resources\layout namespace. If you look at the sample code, there is nothing special there, I simply used some random template I saw somewhere to learn some syntax and made one. I am more familiar with XAML and just tried to match up the containers and controls.

But to me, the more important code is overriding the GetInfoContents method to put useful information in the popup.

public Android.Views.View GetInfoContents(Marker marker
{
    var inflater =
        Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService)
        as Android.Views.LayoutInflater;

    if (inflater != null)
    {
        Android.Views.View view = null;
        var pin = GetCustomPin(marker);
        if (pin != null)
        {
            view = inflater.Inflate(Resource.Layout.CustomInfoWindow, null);

            // find the controls in the layout for the title and subtitle.
            // if we find them, we can populate them with our own info
            var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
            var infoSubTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowSubtitle);

            if (infoTitle != null)
                infoTitle.Text = pin.Pin.Label;

            if (infoSubTitle != null)
                infoSubTitle.Text = pin.Pin.Address;

            return view;
        }
    }

    return null;
}

At this point, if the map were to load up, we should see a pin on the map and be able to tap on it, and show some of our own custom information.

Next, we will talk about how to update the map, and better, do it from the view model.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s