Taoffi's blog

prisonniers du temps

Xamarin forms maps – let’s talk renderers: 2. Droid

Let us go back to Xamarin Forms Maps!

In a previous post, I exposed a WinPhone custom renderer for maps.

The object of our renderer is to:

  • Display custom pushpin (marker) icon
  • Display a custom info window for each place (pushpin / marker)

 

The custom renderer

Our Droid custom renderer will derive from MapRenderer and, for reasons we will see later, will implement the GoogleMap.IInfoWindowAdapter:

 

[assembly: ExportRenderer (typeof(iMaps.iCustomMap),
                                    typeof(iMaps.Droid.iCustomMapViewRenderer))]


namespace iMaps.Droid
{
    public class iCustomMapViewRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter

 

Customizing the marker icon

In android, markers can relatively easily be customized to show a custom icon. Here, we use the custom icon of our Custom pin

 

iCustomPin    customPin    = pin.BindingContext as iCustomPin;
string    resourceName    = System.IO.Path.GetFileNameWithoutExtension(customPin.IconResource);
int        resourceId        = Context.Resources.GetIdentifier(resourceName, "drawable",
                                        Context.PackageName);

bmp = BitmapDescriptorFactory.FromResource(resourceId);
marker.SetIcon(bmp);

Setting markers and their custom icons will be done when the map is to be rendered (i.e. in the OnElementPropertyChanged override of our renderer). That is best done once when the map will be ready.

To know when the map is ready, we must implement the IOnMapReadyCallback Interface which defines one only method: void OnMapReady(GoogleMap googleMap). With this in place, google maps will call us when the map is ready.

Our renderer will thus now look like this:

public class iCustomMapViewRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter,
IOnMapReadyCallback

Our OnMapReady method will set variables required for drawing the map.

 

GoogleMap            _map;

public void OnMapReady (GoogleMap googleMap)
{
    _map        = googleMap;
    _map.InfoWindowClick    += Map_InfoWindowClick;

    // required if you wish to handle info window and its content
    _map.SetInfoWindowAdapter (this);
}

OnElementPropertyChanged of our renderer will proceed to putting the pins on the map:

 

 

bool                _isDrawingDone;

protected override void OnElementPropertyChanged (object sender,

                                        PropertyChangedEventArgs e)
{
    base.OnElementPropertyChanged (sender, e);

    if(_map == null || _isDrawingDone || e.PropertyName != "VisibleRegion")
        return;

    _map.Clear ();
    _map.MarkerClick            += HandleMarkerClick;
    _map.InfoWindowClick    += Map_InfoWindowClick;

    foreach (var pin in _mapPins)
    {
        var            marker        = new MarkerOptions ();
        iCustomPin    customPin    = pin.BindingContext as iCustomPin;

        marker.SetPosition (new LatLng (pin.Position.Latitude, pin.Position.Longitude));
        marker.SetTitle (pin.Label);

        if(! string.IsNullOrEmpty(pin.Address))
            marker.SetSnippet(pin.Address);

        BitmapDescriptor    bmp        = null;

        string    resourceName    =
                    System.IO.Path.GetFileNameWithoutExtension(customPin.IconResource);
        int        resourceId        = Context.Resources.GetIdentifier(resourceName,
                                                "drawable", Context.PackageName);

        bmp = BitmapDescriptorFactory.FromResource(resourceId);
        marker.SetIcon(bmp);

        _map.AddMarker(marker);
    }

    _isDrawingDone = true;
}

 

Customizing the popup info window

Customizing the popup info window is a different kettle of fish ('une autre paire de manche' en françaisJ)

Why?

Simply because the google maps documentation says that the info window is a View, but specify this interesting note:

Note: The info window that is drawn is not a live view. The view is rendered as an image (using View.draw(Canvas)) at the time it is returned. This means that any subsequent changes to the view will not be reflected by the info window on the map.

   

To update the info window later (for example, after an image has loaded), call showInfoWindow(). Furthermore, the info window will not respect any of the interactivity typical for a normal view such as touch or gesture events. However you can listen to a generic click event on the whole info window as described in the section below.

 

So, it IS a View… but NOT REALLYJ

Let us be more specific: it is NOT a view. It is actually a Bitmap.

 

Anyway, to be able to customize the info window, our renderer must implement the GoogleMap.IInfoWindowAdapter Interface. Which defines two methods:

  • View GetInfoContents(Marker marker);
  • View GetInfoWindow(Marker marker);

 

GetInfoWindow: returns the entire popup window. If it returns null, then it is GetInfoContents which will define the contents of the default info window frame of the marker popup.

As the 'pseudo-view' returned in both cases are not really views but bitmaps, our task is to compose a view of the pin information before transforming this to a bitmap.

 

In our current exercise, we will return the window content. Therefor our GetInfoWindow() will return null.

public Android.Views.View GetInfoWindow(Marker marker)
{
    return null;
}

From Xamarin Forms Xaml à to Native View à to Bitmap: dangerous tour!

As I am not a 'droid boy', I decided to create a Xamarin Forms control (ContentView) and use this in the custom renderer. You may of course decide differently to use a Droid axml control. In both ways, we will have to convert that control's content into Bitmap.

Here is my ContentView Xaml (assumed to be bound to an iCustomPin):

 

<StackLayout Spacing="4" >
    <Image Source="{Binding ImageFile}"
            WidthRequest="84" HeightRequest="84" HorizontalOptions="StartAndExpand" />
    <Label FontAttributes="Bold" Text="{Binding Name}" />
    <BoxView HeightRequest="1" BackgroundColor="Gray"
                HorizontalOptions="FillAndExpand" VerticalOptions="Start" />
    <Label x:Name="labelText" Text="{Binding MapPinText}" LineBreakMode="WordWrap" />
</StackLayout>

Our GetInfoContents method will have to:

  • Identify the clicked custom pin
  • Set the binding context of the above ContentView to that pin
  • Transform that XF control to a native Droid ViewGroup
  • Transform the resulting droid native ViewGroup into a bitmap to be returned to Google maps.

The following code illustrates these steps:

 

 

iMapPinInfoCtrl        xamInfoPanel    = new iMapPinInfoCtrl();

public Android.Views.View GetInfoContents(Marker marker)
{
    iCustomMap     myMap         = this.Element as iCustomMap;
    iCustomPin     customPin     = myMap.GetPinAtPosition(
                new Position(marker.Position.Latitude, marker.Position.Longitude));
    double            infoWidth        = this.Control.Width * 0.70,
                        infoHeight        = 380.0 / 2.8;
    SamplePlace    place                = customPin.DataObject as SamplePlace;

    xamInfoPanel.BindingContext = place;

    // get the droid native control
    ViewGroup    viewGrp        = DroidXFUtilities.ConvertFormsToNative(
                xamInfoPanel.Content, new Rectangle(0, 0, infoWidth, infoHeight));

    // transform the native control into a bitmap
    Bitmap        bmp            = DroidXFUtilities.ViewGroupToBitmap(viewGrp,
                                                    this.Context,
                                                    (int)infoWidth, (int)infoHeight,
                                                    true);

    _pinImage.SetImageBitmap(bmp);

    return _pinImage;
}

To Native ViewGroup

An interesting post from Michael Ridland helped solve this task!

Get the native droid ViewGroup of XF View code snippet:

 

public static ViewGroup ConvertFormsToNative(Xamarin.Forms.View view,
                                            Rectangle size)
{
    var        vRenderer        = Platform.CreateRenderer(view);
    var        viewGroup        = vRenderer.ViewGroup;

    vRenderer.Tracker.UpdateLayout();

    var        layoutParams    = new ViewGroup.LayoutParams((int)size.Width, (int)size.Height);

    viewGroup.LayoutParameters        = layoutParams;
    viewGroup.DrawingCacheEnabled    = true;
    view.Layout(size);
    viewGroup.Layout(0, 0, (int)size.Width, (int)size.Height);
    return viewGroup;
}

Native ViewGroup to Droid.Graphics.Bitmap

I must first confess that this task was hard for meJ.

I read some articles about the subject, of which this interesting one "Converting Views to Bitmap Images in Android"… but, in the practical exercise, couldn't grasp how to get precise measures of rendered elements!

The task steps is to:

  • Create a Linear Layout
  • Create a Bitmap and put it into a Canvas
  • Loop through the ViewGroup's elements (Views) and add them to the Layout
  • Draw the Layout into the Canvas
  • Return the Bitmap (which will contain the rendered layout's views)

Getting the precise measures of views is not simple. Notably for images and labels with text wrap attribute.

Here is a simplified snippet of my code (assuming that no images are part of the ViewGroup's elements). The solution's source code has more details about handling images. But you should be able to do much better if you are an android expertJ

 

 

public static Android.Graphics.Bitmap ViewGroupToBitmap(ViewGroup viewGroup,
                                             Context context,
                                             int width, int height)
{
    int        viewCount = viewGroup == null ? 0 : viewGroup.ChildCount;
    Android.Widget.LinearLayout layout        = new Android.Widget.LinearLayout(context);
    Bitmap    bmpLayout    = Bitmap.CreateBitmap(width, height, Bitmap.Config.Argb8888);
    Android.Graphics.Color    white        = Android.Graphics.Color.Argb(0xff, 0xff, 0xff, 0xff);

    layout.DrawingCacheEnabled            = true;
    layout.SetBackgroundColor(white);

    Canvas    canvas    = new Canvas(bmpLayout);

    // add the sub views contained in this view group
    for (int ndx = 0; ndx < viewCount; ndx++)
    {
        Android.Views.View view = viewGroup.GetChildAt(0);

        int            wid            = Math.Max(0, view.MeasuredWidth),
                        hi                = Math.Max(0, view.MeasuredHeight);

        viewGroup.RemoveView(view);
        layout.AddView(view, wid, hi);
    }

    layout.Draw(canvas);

    return bmpLayout;
}

 

The result looks like this:

 

You may Download the code Here

Xamarin forms maps – let’s talk renderers: 1. Win Phone

I continue about XF maps, where a custom renderer is needed if you want to customize appearance and interaction.

As most Xamarin forms samples about maps expose renderers for Droid and iOS (I did not see any about WP), I will start by exposing a sample for that 'missing' renderer. Another post will follow with some ideas about the renderers for the other platforms.

 

In this sample, we handle SamplePlace objects (see below) through our custom map and custom pin objects (see previous post)

What we want:

  • Display custom pushpins (icons specified for each custom pin)
  • When a pushpin is clicked, display a custom information callout about the place.

 

The way to do this:

The renderer (which derives from MapRenderer) receives our custom map, containing the map pins. The binding context of each Pin is set to the related custom pin. The place data is accessed through the custom pin's DataObject.

The renderer has two levels of duties:

  • A 'formal' role inherited from the default renderer: create and display the specific platform map control, position the pins on it and define the interaction with the control's events (map taps / pins taps / callout taps…).
  • A customization role: use our specific embedded information to customize pins' appearance and callouts.

 

The renderer code

Our renderer class:

public class iCustomMapRenderer : ViewRenderer<iCustomMap, Map>


For Xamarin.Forms.Maps to retrieve our renderer at runtime, it should be 'exported'. The code should now be like the following:

 

// Note: ExportRendererAttribute is defined in Xamarin.Forms namespace
[assembly: ExportRenderer(typeof(iCustomMap), typeof(iCustomMapRenderer))]

namespace iMaps.WinPhone
{
	public class iCustomMapRenderer : ViewRenderer<iCustomMap, Map>
	{
		private Map				_winMap;			// the win phone map control
		private iCustomMap		_xfMap;				// the xamarin forms map we are handling
		private iCustomPin		_selectedPin;	// our current slected pin

		// the callout user control that will display selected place info
		UserControl	_placeInfoCtrl	= new UserControl()
		{
			HorizontalAlignment	= HorizontalAlignment.Stretch,
			VerticalAlignment		= VerticalAlignment.Stretch,
			MinHeight			= 300,
			Background			= whiteBrush,
		};
		…



 

Here is a global view of the renderer's methods:

 

In the ElementPropertyChanged (override), we will:

  • Create the win phone map control.
  • Parse the received pins, and use their specified icons as pushpins.
  • Subscribe to each pushpin Tap event to display the callout.
  • Subscribe to the map Tap event to hide any displayed callout.

 

// Center the map on the 1st pin
var			pin	= _xfMap.Pins[0];

// create the win phone map control. set as the native control
_winMap		= new Map
		{
			ZoomLevel = 13,
			Center = new GeoCoordinate(pin.Position.Latitude, pin.Position.Longitude)
		};

this.SetNativeControl(_winMap);

// subscribe to map Tap event (to unselect last pin if any)
_winMap.Tap		+= WinMap_Tap;

// AddCurrentLocationToMap();
// loop through the received pins. display each with its custom pin icon
foreach (var formsPin in _xfMap.Pins)		//.CustomPins)
{
	var			pushPin		= new Pushpin();
	iCustomPin	customPin	= formsPin.BindingContext as iCustomPin;
	SamplePlace	place		= customPin == null ? null 
										: customPin.DataObject as SamplePlace;

	// subscribe to the pushpin tap event (to display the callout)
	pushPin.Tap += PushPin_Tap;

	// set the pushpin position on the map
	var geoCoordinate			= new GeoCoordinate(formsPin.Position.Latitude, formsPin.Position.Longitude);
	pushPin.GeoCoordinate		= geoCoordinate;

	// set the pushpin tag to the custom pin
	pushPin.Tag					= customPin;
	…
	…



The custom pushpin icon

To put a UI element on the map, you create a MapOverlay containing the element on the desired location (geo coordinate) and you add that MapOverlay to a Layer that you put on the map.

A Pushpin (defined in Microsoft.Phone.Maps.Toolkit namespace) is a UI element that exposes a Content property (of type ContentControl) which we can set to our image of choice (the one specified by the custom pin object for instance). In that case, the pushpin would look like this:

We may also directly put our image on the MapOverlay. In that case that would be like this:

 

The callout control

Let us create a (win phone) xaml UserControl for displaying place object's information. We will use this when a pushpin will be tapped.

 

<Border x:Name="LayoutBorder" Background="Transparent" Height="auto" Padding="8" >
	<Grid VerticalAlignment="Top" Background="Transparent">
		<Grid.RowDefinitions>
			<RowDefinition Height="24" />
			<RowDefinition />
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="24" />
			<ColumnDefinition />
		</Grid.ColumnDefinitions>

		<Image Source="{Binding Source, Source={StaticResource imgMapPointer}}" Grid.Column="0" Grid.Row="0" Stretch="Fill" Height="24" Width="24" HorizontalAlignment="Left" VerticalAlignment="Top" />

		<Border Padding="8" Background="White" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2">
			<StackPanel>
				<Image x:Name="imgPlace" Height="128" Width="128" HorizontalAlignment="Left" Source="{Binding Converter={StaticResource placeImageConverter}}" Margin="8" />
				<TextBlock FontWeight="Bold" Text="{Binding Name}" Foreground="Black" />
				<Rectangle Height="1" Fill="Black" HorizontalAlignment="Stretch" />
				<TextBlock Margin="8,0" FontSize="14" Text="{Binding MapPinText}" Foreground="Black" />
			</StackPanel>
		</Border>
	</Grid>
</Border>



 

At design time, the code above would look like:

At runtime, the Pushpin Tap event handler will set the control's DataContext to the selected SamplePlace object. It will then display its image and information.

The pushpin Tap event handler determines the currently selected place (through the pushpin.Tag) and calls this method to update the control:

 

void UpdateInfoControlContents(SamplePlace place)
{
	_placeInfoCtrl.ClearValue(UserControl.ContentProperty);

	if(place == null)
		return;

	PlacePushPinInfoCtrl ctrl = new PlacePushPinInfoCtrl() { DataContext = place };

	_placeInfoCtrl.Content = ctrl;
}



 

 

There is certainly still much to do for these mechanics to produce something elegant and informative.

That may just be a good start.... Download the code and have more fun!

In a following post I will talk about some ideas for Droid and iOS map renderers (also included in the sample).

Xamarin forms – what about maps?

Why do we use maps?

Apart from the pleasure (and poesy) of locating things in the global earth space, there is something practical and useful in using maps and geolocation.

I myself could never be able to live in big cities without mapping applications guiding me from a place to another and giving me such vital information like 'where can I park my car' (almost a dilemma in European cities at leastJ)

As we talk about maps, we of course talk about mobility (and vice versa). That puts it in a way where we might think that a mobile app is often linked to mapping features: if your app sells something (it often doesJ), the user will inevitably ask: where is it? How can I go there?... etc.

Xamarin forms maps package

The Xamarin forms maps package delivers a good solution to integrate maps into your XF applications.

A nice sample is delivered that explains how to get started with the package.

To avoid some of startup pitfalls, I compiled my own pitfalls and solutions here.

The package (Xamarin.Forms.Maps.dll) contains a few useful objects:

 

Xamarin.Forms.Maps - What / Who lives there?

The component exposes three main objects:

  • Map object (a View)
  • MapSpan object
  • Pin object (a BindableObject)

Finally an object: Geocoder for geocoding operations (that I didn't find really useful or simply operational)

More details and Inheritance

A Map (deriving from View) is of:

  • MapType (enumeration)
  • It has two MapSpan(s) representing the VisibleRegion and the LastMoveToRegion
  • And it contains a collection of Pin(s) (IList<Pin>)

A MapSpan is composed of its center (a Position: a structure of Latitude, Longitude), dimensions in terms of Distance (a structure with some factory methods (FromKilometers, FromMiles…) to apply values using known distance units).

The good news is that the Pin is a BindableObject. We will see how to use this to extend some features.

Extending Xamarin Forms Maps

You can simply use the maps package with its provided features and obtain good and useful results for your app's users: display pins on the map, when a pin is clicked, its label is displayed, and you can respond to that label's Tap event to open your selected item's specific form. That can be enough in many cases.

Other more demanding apps may need, for instance, to customize pin appearance and/or the callout for each pins… etc.

The standard Pin object is limited to handling objects' Address (string), Label (string), Position (coordinates) and pin Type (enumeration: Generic, Place, SavedPin or SearchResult). It also exposes a Clicked event to which your app can subscribe.

 

It does not handle other features that you may need… like specifying a pin icon, or a custom callout.

So, you think: still, we can derive a custom pin object that may provide such requested information.

Unfortunately that is not possible because Pin is a sealed class! (Why is it sealed?... mystery!. I hope the creators have good reason for this choice. For now, I find this a little strange)

Customizing maps: yet another solution

I found some interesting efforts to extend and circumvent the Xamarin maps limitations.

You may have a look at some Here… or Here.

Most efforts concentrate on offering Custom rendering solutions (For Droid and iOS. Few, if any, expose a Win Phone workable renderer).

The fact that we need a custom renderer for maps is inevitable if we opt to using the specific platform mapping features (which seems the reasonable choice). A structured (architectural) approach is required in any case.

What we want to handle in this sample, is a Place object:

It contains information about its location (latitude, longitude, address…) and other properties (Name, description, image…)

What we need in our current sample is:

  • Display a custom pushpin according to the type (category) of the Place at the pin location.
  • Display a custom callout when the pin would be clicked.
  • Raise a Clicked event when the callout would be tapped.

Instead of this:

We would like to have something like this :

As we cannot inherit the Pin class (sealed), let us create an iCustomPin class containing the target map Pin and providing the required customization features (here the pin's icon):

The iCustomPin class will act as a view model for the Pin, thus providing some (gets) of its properties: MapPinLabel, MapPinPosition, MapPinType… etc.

It exposes a DataObject (an 'object', which will contain a Place object in the current sample… Note: That may be refactored to a BindableProperty)

An iCustomMap object (deriving from the Xamarin forms Map) will be in charge of inserting / updating our custom pins. It may also provide a custom Callout control template (ContentView) that can be used for displaying the selected (Clicked) pushpin.

How these objects can be used?

The sample execution sequence can be as follows:

  • Data: create a list of sample places, each containing coordinates, description…
  • View: Insert a custom map control (iCustomMap object) into the page
  • For each ample place, add the corresponding custom pin (iCustomPin) to the custom map:
    • A custom pin contains a 'standard' Pin
    • Set standard Pin's BindingContext to the custom pin. (The custom pin contains the Data Object… which is the Place)
  • Rendering: the custom renderer, at each platform, can access the custom pin info (and thus the sample Place) through the Pin's BindingContext… It would then display the pushpins according to the specified icon asset, and eventually displays the place's callout when required.

 

 

 

In a following post, I will expose custom renderers details for Android and WP.

Will also try to polish the sample code a little more before publishing for download. Will keep you posted when readyJ

Xamarin Forms Maps – a (very) quick start!

Solutions to avoid some pitfalls for starting up with Xamarin forms maps package.

Before to use?

Some steps are required for your map application to be able to start and to display maps.

 

In Android project, before to use maps, you must first generate an API key. This can be done in your google account (create one if needed). You can optionally in this step link your API key to a specific application. This API key should be specified in the AndroidManifest.xml of your Android project:

<application android:label="Your app name" android:icon="@drawable/your icon">
    <meta-data android:name="com.google.android.geo.API_KEY"
            android:value="put you api ur key" />
<meta-data android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />
</application>


In this same file, you should also specify the required location access permissions:

 

For iOS, you should specify the some values for the following keys in info.plist file:

<key>NSLocationAlwaysUsageDescription</key>
<string>Please allow us to use your location.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We are using your location</string>


For WinPhone, you should specify some application capabilities as (at least):

 

For All platforms:

In each platform, call Xamarin.FormsMaps.Init(); after Xamarin.Forms.Forms.Init()

Without this call, your map will not show!

  • iOS - AppDelegate.cs file, in the FinishedLaunching method.
  • Android - MainActivity.cs file, in the OnCreate method.
  • Win Phone - MainPage.xaml.cs file, in the MainPage constructor.

 

That is what the Xamarin Forms Maps documentation saysJ. In fact I tested: that is required for iOS, not required for Android… and for Win Phone… just a minute… the emulator is starting up… IT IS required!

 

How to use?… Look at it working

To use these items, you simply insert a Map View in your Xaml page (or ContentView). Which can also be done in code. In xaml, don't forget to mention the namespace!

Here is an example of a ContentView (a UserControl for our WPF/Silverlight friends!)

<?xml version="1.0" encoding="UTF-8"?> 
<ContentView    x:Class="iMaps.iMapCtrl1" 
    xmlns="http://xamarin.com/schemas/2014/forms" 
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
    xmlns:map="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps" 
    > 
    <map:Map x:Name="theMap" /> 
</ContentView> 

 

Important remark: for a mysterious reason (you will see many in Xamarin environment), if the 'map' namespace specifies the assembly's extension ('.dll') that doesn't work in WinPhone (i.e. app crash!)

Should write this:

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

Not this (results in WinPhone crash):

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

 

Now, for this sample, we can insert some Pins at the ContentView (UserControl) creation:

public iMapCtrl1()
{
    InitializeComponent();
    PutSomePinsOnMap();
}

 

void PutSomePinsOnMap()
{
    // define a center point and some sample pins
    Position        tourEiffel    = new Position(48.859217, 2.293914);
    Pin[]        pins            =
    {
        new Pin() {  Label = "Tour Eiffel",
            Position = new Position(48.858234, 2.293774), Type = PinType.Place },
        new Pin() {  Label = "Concorde",
            Position = new Position(48.865475, 2.321142), Type = PinType.Place },
        new Pin() {  Label = "Étoile",
            Position = new Position(48.873880, 2.295101), Type = PinType.Place },
        new Pin() {  Label = "La Défense",
            Position = new Position(48.892418, 2.236180), Type = PinType.Place },
    };

    foreach(Pin p in pins)
    {
        theMap.Pins.Add(p);
    }

    // center the map on Tour Eiffel / set the zoom level
    theMap.MoveToRegion(MapSpan.FromCenterAndRadius(tourEiffel, Distance.FromKilometers(2.5)));
}
 

 

Once this ContentView in a Page that gives a nice view like this: