Isaac Ojeda
Isaac Ojeda

Reputation: 325

Xamarin.Forms.Maps 2.3.4 custom MapRenderer disables everything

My problem occurs after I updated Xamarin.Forms and Xamarin.Forms.Maps to the new version (2.3.4).

After that I also updated all google play services in Android project (and a lot of libraries that I hate).

The main problem is that I have a custom MapRenderer for custom pins, in iOS and UWP works fine, but in Android version this custom MapRenderer brokes all the Map. Any property change or method call seems to be ignored.

For example I have a button to toggle the map type (Hybrid or Street) and that action never changes it. I also noticed (according this tutorial: https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/map/customized-pin/) that the property "VisibleRegion" never changes so the following code never executes:

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

        if (e.PropertyName.Equals("VisibleRegion") && !_isDrawn)
        {
            // Do something with your custom map
        }
    }

Inside that if i used to populate my custom pins (like the tutorial above) and now my Map is always empty.

Now i populate my map inside the IOnMapReadyCallback and it works fine, but the I still have the bindings problem.

If I ignore the custom MapRendered (removing the assembly line) all the bindings starts working fine but my map now have the old pins and all customization is gone (obviously).

In the PCL I have things like MyMap.MoveToRegion(...) and MyMap.MapType = _currentType; but those instructions only works if a don't use a custom MapRenderer.

My custom MapRenderer is almost the same as the tutorial above.

The custom Map is created with C# and not with XAML, it doesn't have any XAML binding but any property change or method call like the MoveToRegion or MapType is totally ignored if i'm using the MapRenderer.

Any help?

Thanks

Upvotes: 11

Views: 2940

Answers (2)

Nicolas Bodin
Nicolas Bodin

Reputation: 1591

I got the same issue and I solved it thanks to this answer on a Xamarin Forum.

This is my map renderer (Android part) to replace the marker's image of a pin :

[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace MyNamespace.Droid
{
    public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
    {
        GoogleMap map;
        List<CustomPin> customPins;
        bool isDrawn;

        protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null) 
            {
                map.InfoWindowClick -= OnInfoWindowClick;
            }

            if (e.NewElement != null)
            {
                var formsMap = (CustomMap)e.NewElement;
                customPins = formsMap.CustomPins;
                Control.GetMapAsync(this);
            }
        }

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if (e.PropertyName.Equals ("VisibleRegion") && !isDrawn) {
                map.Clear ();
                foreach (var pin in customPins) {
                    var marker = new MarkerOptions();
                    marker.SetPosition (new LatLng(pin.Pin.Position.Latitude, pin.Pin.Position.Longitude));
                    marker.SetTitle (pin.Pin.Label);
                    marker.SetSnippet (pin.Pin.Address);
                    marker.SetIcon (BitmapDescriptorFactory.FromResource (Resource.Drawable.fake_ic_pin));

                    map.AddMarker (marker);
                }
                isDrawn = true;
            }
        }

        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            base.OnLayout(changed, l, t, r, b);

            if (changed)
            {
                isDrawn = false;
            }
        }

        void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
        {
            var customPin = GetCustomPin(e.Marker);
            if (customPin == null)
            {
                throw new Exception("Custom pin not found");
            }

            if (!string.IsNullOrWhiteSpace(customPin.Url))
            {
                var url = Android.Net.Uri.Parse(customPin.Url);
                var intent = new Intent(Intent.ActionView, url);
                intent.AddFlags(ActivityFlags.NewTask);
                Android.App.Application.Context.StartActivity(intent);
            }
        }

        void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap) 
        { 
            InvokeOnMapReadyBaseClassHack(googleMap); 
            map = googleMap;
            map.SetInfoWindowAdapter(this);
            map.InfoWindowClick += OnInfoWindowClick;
        }

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

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

        CustomPin GetCustomPin(Marker annotation)
        {
            var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
            foreach (var pin in customPins)
            {
                if (pin.Pin.Position == position)
                {
                    return pin;
                }
            }
            return null;
        }

        void InvokeOnMapReadyBaseClassHack(GoogleMap googleMap)
        {
            System.Reflection.MethodInfo onMapReadyMethodInfo = null;

            Type baseType = typeof(MapRenderer);
            foreach (var currentMethod in baseType.GetMethods(System.Reflection.BindingFlags.NonPublic |
                                                             System.Reflection.BindingFlags.Instance |
                                                              System.Reflection.BindingFlags.DeclaredOnly))
            {
                if (currentMethod.IsFinal && currentMethod.IsPrivate)
                {
                    if (string.Equals(currentMethod.Name, "OnMapReady", StringComparison.Ordinal))
                    {
                        onMapReadyMethodInfo = currentMethod;
                        break;
                    }

                    if (currentMethod.Name.EndsWith(".OnMapReady", StringComparison.Ordinal))
                    {
                        onMapReadyMethodInfo = currentMethod;
                        break;
                    }
                }
            }

            if (onMapReadyMethodInfo != null)
            {
                onMapReadyMethodInfo.Invoke(this, new[] { googleMap });
            }
        }
    }
}

Upvotes: 0

Isaac Ojeda
Isaac Ojeda

Reputation: 325

I already found the solution.

Looking at the source code, MapRenderer already implements IOnMapReadyCallback and if you remove the implementation in the custom MapRendered, everything starts working again (but with no customization).

MapRenderer saves the google map instance in the property NativeMap (also exists the property Map that is the Xamarin forms map instance) so we don't need to implement IOnMapReadyCallback any more. I think we need to be careful in the use of NativeMap because at the begining it could be null.

In the method I mentioned before now i do this:

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

        if (e.PropertyName.Equals("VisibleRegion") && !_isDrawn)
        {
            PopulateMap();
            OnGoogleMapReady();
        }
    }

and the code I had in OnMapReady now goes inside OnGoogleMapReady():

    private void OnGoogleMapReady()
    {
        if (_mapReady) return;

        NativeMap.InfoWindowClick += OnInfoWindowClick;
        NativeMap.SetInfoWindowAdapter(this);

        _mapReady = true;
    }

I also added this in OnElementChanged to remove any registered delegate in NativeMap

        if (e.OldElement != null)
        {
            NativeMap.InfoWindowClick -= OnInfoWindowClick;
        }

At the moment exists a Pull Request that implements OnMapReady as virtual method, so we can override it in our implementation and now be sure when NativeMap is not null, but for that we need to wait for a next release.

You can read more here -> https://forums.xamarin.com/discussion/92565/android-ionmapreadycallback-forms-2-3-4

Upvotes: 4

Related Questions