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

Comments are closed