Custom Map Renderer on Android: Part 3

This is the final post in the series about custom map renderers on Android. To recap, we added a map page to our application and then customized the appearance of the map markers and the info window that is shown when the user taps a marker.

Now we are going to make the map a bit more usable by making it possible to update the pins from the view model, and making it easy to consume services in the Android specific project.

In the previous post, we defined a CustomMap control that only had one property:


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

The property above is only available from the ContentPage code behind. We are going to change this to be a bindable property that can be bound in the view model itself.


public class CustomMap : Map
{
    public static readonly BindableProperty CustomPinsProperty =
        BindableProperty.Create(
            "CustomPins",
            typeof(List<CustomPin>),
            typeof(CustomMap),
            new List<CustomPin>(),
            BindingMode.TwoWay);

    public List<CustomPin> CustomPins
    {
        get { return (List<CustomPin>) GetValue(CustomPinsProperty); }
        set { SetValue(CustomPinsProperty, value);
    }
}

Now that CustomPins is a bindable property it can participate in the data binding system. We can setup a property in our view model and attach it in our XAML.


    <view:CustomMap x:Name="cusmap"
IsShowingUser="True" MapType="Street"
CustomPins="{Binding CustomPins, Mode=TwoWay}" />

And here is the property in the view model:


/// other properties
private List<CustomPin> _customPins = new List<CustomPin>();
public List<CustomPin> CustomPins
{
    get { return _customPins; }
    set { SetProperty<List<CustomPin>>(ref _customPins, value); }
}

Finally, we need to get our CustomMapRenderer to react to these changes. The way we do that is to override the OnElementPropertyChangedEvent to list for changes in the CustomPins property.


public class CustomMapRenderer : MapRenderer, GoogleMap.IInfowindowAdapter, IOnMapReadyCallback
{
    // .... all the other code

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (Element == null || Control == null)
            return;

        if (e.PropertyName == CustomMap.CustomPinsProperty.PropertyName)
        {
            UpdatePins();
        }
    }
}

Finally, I want to show how you can use that data binding technique to pass some services through to your Android project. For example, when a user taps a marker on the map, the info window will show. We can listen for when the user taps on that info window and use our Prism navigation service to pull up a new page. Let’s add the new bindable property to the CustomMap class.


public class CustomMap : Map
{
    public static readonly BindableProperty CustomPinsProperty =
        BindableProperty.Create(
            "CustomPins",
            typeof(List<CustomPin>),
            typeof(CustomMap),
            new List<CustomPin>(),
            BindingMode.TwoWay);

    public List<CustomPin> CustomPins
    {
        get { return (List<CustomPin>) GetValue(CustomPinsProperty); }
        set { SetValue(CustomPinsProperty, value); }
    }

    public static readonly BindableProperty NavigationServiceProperty =
        BindableProperty.Create(
            "NavigationService",
            typeof(Prism.Navigation.INavigationService),
            typeof(CustomMap),
            null);

    public Prism.Navigation.INavigationService NavigationService
    {
        get { return (Prism.Navigation.INavigationService) GetValue(NavigationServiceProperty); }
        set { SetValue(NavigationServiceProperty, value); }
    }
}

Let’s make sure the navigation service is exposed in our view model and then bind it in XAML. Here’s the view model:


public class MapViewModel : BaseViewModel
{
    public MapViewModel(INavigationService navigationService)
        : base()
    {
        NavService = navigationService;
    }

    /// other properties, such as CustomPins

    private INavigationService _navService = null;
    public INavigationService NavService
    {
        get { return _navService; }
        set { SetProperty<INavigationService>(ref _navService, value); }
    }

    /// other code is here
}

And now the XAML:


    <view:CustomMap x:Name="cusmap"
IsShowingUser="True" MapType="Street"
CustomPins="{Binding CustomPins, Mode=TwoWay}"
NavigationService="{Binding NavService}" />

And now we can get use the navigation service from our CustomMapRenderer like this by adding in our InfoWindowClick handler. Remember that we wired it up in the OnMapReady function. Now let’s put some functionality in the handler:


private void OnMapInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
    if (_customMap.NavigationService != null)
    {
        _customMap.NavigationService.NavigateAsync(DevDaysSpeakers.PageKeys.Speakers);
    }
}

You can find all of this code in my GitHub repo in the MapRenderer solution. If you run the project, you can tap on the “Map” button on the main page. It will bring up the map and display a marker. If you tap the test button, it will update the marker to a new location. Tapping on the marker will show a custom info window, and tapping that window will use the Prism navigation service to navigate back to the main page.

Sounds like I am finally going to pick up a Mac laptop, so I should be able to expand these posts to the iOS platform. Looking forward to that.

Lots of stuff! Hope you find some of it useful.

Xamarin Forms Maps Bits

I ran across a couple of small issues tonight with my work with the Xamarin Forms Map, and it had to do with the precision of the values that I was using for the Center property that I setup in my previous post.

Normally in the Android emulator with Visual Studio, it has a preset starting location. For my app, I wanted something a little closer to home. To get the values for the MapSpan object, I was just outputting the values for the fields from the Center property that I had setup to figure out where I wanted to start from.

When I used those values as my initial start point for the map, I was putting in the full precision that the Visual Studio debugger was showing, approximately 15 digits after the decimal place. When I started forcing these values in, the map wouldn’t update its position.

I then started to play around with the location services in the emulator and the map would update in response to those values. Looking at the values displayed by the emulator, they only used 4 digits. I made the change and everything started working again. Assuming I am in the view model and am using the attached property to bind to set the center point of the map, this didn’t work for me:


// underlying Map.MoveToRegion call doesn't do anything
Center = new MapSpan
(
	new Position(48.0111563131213188, -122.0096835170062306),
	0.0096835170062306,
	0.0111563131213188
);

Instead, try using it like this:


// underlying Map.MoveToRegion call now works
Center = new MapSpan
(
	new Position(48.0111, -122.0096),
	0.0096,
	0.0111
);

I have yet to go looking at why this is the case but it seems likely that the map control can only handle that level of accuracy. If you are finding that you can’t seem to control what the map is displaying, check out the precision of the values in the MapSpan object. And if anyone reading this knows for sure what is happening here, feel free to chime in.

I will be attending a Xamarin dev workshop at Microsoft Vancouver, perhaps there will be someone there that can answer this definitely.

Hope that helps.

Xamarin Forms Map Center

Problem:

I have been working on an app in Xamarin, with the goal of being able to deploy to UWP, Android and IOS. This is my first real effort with Xamarin, and the first time publishing IOS and Android to their respective stores. Actually, this is my first IOS app ever! I still need to get on to Craigslist and find a reasonably priced Mac Mini so that I can build the IOS app.

Some app background: it makes use of the map control. My needs aren’t too difficult, but I need to know what the map control is displaying at any given moment. Specifically, I need the latitude and longitude of the map center. At least at this point, my app doesn’t need to worry about location tracking.

I am going to use Xamarin forms and take advantage of the cross platform UI. I also want to make use of the MVVM pattern and the data binding associated with XAML.I will of course use my favorite MVVM framework: Prism (I am going to do a series on Prism and UWP and a series on Prism and Xamarin Forms as well at some point). All of this sounds great: but unfortunately the map center data structure isn’t exposed as a bindable property, so we can’t use it directly in our view model.

Well, since every C#/XAML blog person needs an attached property post, that is what I decided to do. I am not sure that it is the best solution, but I can say that it seems to be working for me right now. Look for an update on this post if it doesn’t!

Solution:

I decided I wanted two properties. One that turned the tracking on and off and one that stored the information about what the map was viewing.

Attached properties are all kind of the same, so first we will just setup the basics (if you need a bit of a refresher on attached properties, check out the Xamarin docs):

  • Creation
  • Getter/Setter
  • Change event
public static class MapCenterProperty
{
	public static readonly BindableProperty CenterProperty =
		BindableProperty.CreateAttached(
			"Center",
			typeof(MapSpan),
			typeof(MapCenterProperty),
			null,
			propertyChanged: OnCenterChanged);

	public static MapSpan GetCenter(BindableObject view)
	{
		return (MapSpan) view.GetValue(CenterProperty);
	}

	public static void SetCenter(
		BindableObject view, MapSpan mapSpan)
	{
		view.SetValue(CenterProperty, mapSpan);
	}

	private static void OnCenterChanged(
		BindableObject view, object oldValue, object newValue)
	{
	}

	public static readonly BindableProperty TrackCenterProperty =
		BindableProperty.CreateAttached(
			"TrackCenter",
			typeof(bool),
			typeof(MapCenterProperty),
			false,
			propertyChanged: OnTrackCenterChanged);

	public static bool GetTrackCenter(BindableObject view)
	{
		return (bool) view.GetValue(TrackCenterProperty);
	}

	public static void SetTrackCenter(
		BindableObject view, bool value)
	{
		view.SetValue(TrackCenterProperty, value);
	}

	private static void OnTrackCenterChanged(
		BindableObject view, object oldValue, object newValue)
	{
	}
}

So let’s take a look at the TrackCenterProperty first. This is the one that is used to toggle the tracking off and on. The important stuff happens in the OnTrackCenterChanged event: it sets up the tracking. If the old value is false and the new value is true, start tracking. Stop tracking if the reverse is true.

Now it would be great if the Map control had an event that fired when the view of the map changed, but it doesn’t. So that is a bit of an issue. Luckily there is a bit of a hack we can use. We are interested in when the “VisibleRegion” property changes. So we are going to subscribe to the “PropertyChanged” event instead. If the name of the property being changed is “VisibleRegion”, we want to know about it.

Let’s update the OnTrackCenterChanged method from up above to subscribe and unsubscribe depending on the value of TrackCenter. It could look like the following:


private static void OnTrackCenterChanged(
	BindableObject view, object oldValue, object newValue)
{
    Map map = view as Map;
    if (map == null)
        return;

    bool ov = (bool) oldValue;
    bool nv = (bool) newValue;

    if (ov && !nv)
    {
        /// was true, now false, unsubscribe
        map.PropertyChanged -= OnMapPropertyChanged;
    }
    else if (!ov && nv)
    {
        /// was false, now true, subscribe
        map.PropertyChanged += OnMapPropertyChanged;
    }
}

All we are doing is subscribing or unsubscribing to the PropertyChanged event depending on the value of TrackCenter. Below is the event handler for that event. The only thing it really does is check to see if it is the VisibleRegion property being changed and if so, update the value of the CenterProperty.


private static void OnMapPropertyChanged(
	object sender, PropertyChangedEventArgs a)
{
    Map map = sender as Map;
    if (map == null)
        return;

    if (a.PropertyName == "VisibleRegion"
        && map.VisibleRegion != null)
    {
        Debug.WriteLine(&quot;SetCenter&quot;);
        map.SetValue(CenterProperty, map.VisibleRegion);
    }
}

Now all we have to do is setup the OnCenterChanged event. The only thing we have to be concerned about is giving ourselves a bit of fuzziness on whether the center point is changed enough to update the map, otherwise I found that the center point property was being updated too often.

private static void OnCenterChanged(
    BindableObject view, object oldValue, object newValue)
{
    var map = view as Map;
    if (map == null)
        return;

    MapSpan newview = newValue as MapSpan;
    MapSpan oldView = oldValue as MapSpan;

    if (newview != null &&
        !Util.MapSpanHelper.IsEqual(oldView, newview))
    {
        map.MoveToRegion(newview);
        SetCenter(view, newview);
    }
}

And the helper function for checking if the MapSpan has changed enough:

public static class MapSpanHelper
{
    public static bool IsEqual(double d1, double d2)
    {
        double dif = Math.Abs(d1 * 0.00001);
        return (Math.Abs((d1 - d2)) <= dif);
    }

    public static bool IsEqual(MapSpan ms1, MapSpan ms2)
    {
        if (ms1 == null && ms2 == null)
            return true;
        else if (ms1 == null || ms2 == null)
            return false;
        else if (
            IsEqual(ms1.Center.Latitude, ms2.Center.Latitude)
            && IsEqual(ms1.Center.Longitude, ms2.Center.Longitude)
            && IsEqual(ms1.LatitudeDegrees, ms2.LatitudeDegrees)
            && IsEqual(ms1.LongitudeDegrees, ms2.LongitudeDegrees))
            return true;
        else
            return false;
    }
}

You can adjust the fuzziness as appropriate.

Finally, we can setup our XAML and view model. Remember to add the namespaces. The MapCenterProperty class exists in the “Demo.Views” assembly.


xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"

xmlns:loc="clr-namespace:Demo.Views;assembly=Demo"

<maps:Map MapType="Street" loc:MapCenterProperty.TrackCenter="True" loc:MapCenterProperty.Center="{Binding Center,Mode=TwoWay}">

</maps:Map>

And the view model:


/// BindableBase provided by Prism, implements the
/// INotifyPropertyChanged interface for the view model
/// and some helper functions
public class DemoViewModel : BindableBase
{
    // ...

    private MapSpan _center = null;
    public MapSpan Center
    {
        get { return _center; }
        set { SetProperty<MapSpan>(ref _center, value); }
    }
}

And there we have it. We now have the viewable region of the map control bound to our view model. From within the view model we can change what is being shown on the map simply by setting the Center property. In the case of my app, I am also going to make use of the GeoLocator plugin from James Montemagno to provide some pretty nice location services to go with the maps in my app.

Hopefully you found this useful.