Since a long time, I'm trying to make work a polyline for crossplateform using.
I did it and it works well, I followed the Map Control tutorial in first, and then Highlight a Route on a Map tutorial.
I then update the code to make it reloads if a any changes comes, however, I'm getting an issue and I couldn't figure it out... It does works for Android & iOS.
polyline.Path = new Geopath(coordinates); throws Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))
The problem is that my two others renderer (Android & iOS) works.. Maybe something isn't possible because I work with WinPhone8.1 unlike the tutorial, which is UWP.
public class CustomMapRenderer : MapRenderer
{
MapControl nativeMap;
CustomMap formsMap;
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
nativeMap = Control as MapControl;
}
if (e.NewElement != null)
{
formsMap = (CustomMap)e.NewElement;
nativeMap = Control as MapControl;
UpdatePolyLine();
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
if (nativeMap != null)
{
var coordinates = new List<BasicGeoposition>();
foreach (var position in formsMap.RouteCoordinates)
{
coordinates.Add(new BasicGeoposition() { Latitude = position.Latitude, Longitude = position.Longitude });
}
var polyline = new MapPolyline();
polyline.StrokeColor = Color.FromArgb(128, 255, 0, 0);
polyline.StrokeThickness = 5;
polyline.Path = new Geopath(coordinates);
nativeMap.MapElements.Add(polyline);
}
}
}
I also read that a key is needed, so maybe I doesn't use this key in a good way.. I tried with UWP Public Key and WinPhone8.X and earlier Key, but without success too..
Does someone has an idea? This part a really big problem in my app..
Thank in advance !
After a long time, I found why it didn't works...
If you're doing like me, if you do a HttpRequest to something as Google Direction API, then the result will comes after the first passage in the OnElementChanged().
Because formsMap.RouteCoordinates isn't null but empty, the Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED)) is thrown..
This is the good CustomMapRenderer for PolyLine use
using PROJECT;
using PROJECT.UWP;
using System.Collections.Generic;
using Windows.Devices.Geolocation;
using Windows.UI.Xaml.Controls.Maps;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.UWP;
using Xamarin.Forms.Platform.UWP;
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace PROJECT.UWP
{
public class CustomMapRenderer : MapRenderer
{
MapControl nativeMap;
CustomMap formsMap;
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
nativeMap = Control as MapControl;
}
if (e.NewElement != null)
{
formsMap = (CustomMap)e.NewElement;
nativeMap = Control as MapControl;
UpdatePolyLine();
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
if (formsMap != null && formsMap.RouteCoordinates.Count > 0)
{
List<BasicGeoposition> coordinates = new List<BasicGeoposition>();
foreach (var position in formsMap.RouteCoordinates)
{
coordinates.Add(new BasicGeoposition() { Latitude = position.Latitude, Longitude = position.Longitude });
}
Geopath path = new Geopath(coordinates);
MapPolyline polyline = new MapPolyline();
polyline.StrokeColor = Windows.UI.Color.FromArgb(128, 255, 0, 0);
polyline.StrokeThickness = 5;
polyline.Path = path;
nativeMap.MapElements.Add(polyline);
}
}
}
}
Related
I am following article on MS page about Custom map renderer and I can not get it to work for android. Map shows but pins (Markers) are not there. I did not found what I did different from official documents (as in previous link). When I debug, there are 3 markers in customPins collection. And custom renderer works in UWP. So issue is only in Android code.
Here is my CustomRender code for Android which does not shows pins.
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Android.Widget;
using iVanApp;
using iVanApp.Android.CustomMapRenderers;
using iVanApp.Droid;
using iVanApp.Model;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.Android;
[assembly: ExportRenderer(typeof(NightMap), typeof(MapRenderers))]
namespace iVanApp.Android.CustomMapRenderers
{
public class MapRenderers : MapRenderer, GoogleMap.IInfoWindowAdapter
{
List<NightPin> customPins;
public MapRenderers(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
NativeMap.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (NightMap)e.NewElement;
customPins = formsMap.NightPins;
}
}
protected override void OnMapReady(GoogleMap map)
{
base.OnMapReady(map);
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
}
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.pin));
return marker;
}
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.Name))
{
}
}
NightPin GetCustomPin(Marker annotation)
{
var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
foreach (var pin in customPins)
{
if (pin.Position == position)
{
return pin;
}
}
return null;
}
public global::Android.Views.View GetInfoContents(Marker marker)
{
var inflater = global::Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as global::Android.Views.LayoutInflater;
if (inflater != null)
{
global::Android.Views.View view;
var customPin = GetCustomPin(marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
view = inflater.Inflate(Resource.Layout.mtrl_layout_snackbar_include, null);
if (customPin.Name.Equals("Xamarin"))
{
view = inflater.Inflate(Resource.Layout.XamarinMapInfoWindow, null);
}
else
{
view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
}
var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
var infoSubtitle = view.FindViewById<TextView>(Resource.Id.InfoWindowSubtitle);
if (infoTitle != null)
{
infoTitle.Text = marker.Title;
}
if (infoSubtitle != null)
{
infoSubtitle.Text = marker.Snippet;
}
return view;
}
return null;
}
public global::Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
}
}
DOes anyone have idea why this would not work?
Note: I had to modify official MS code because I got errors. Wherever is Android.Views.View I had to modify it to global::Android.Views.View otherwise I got following error
"'MapRenderers' does not implement interface member
'GoogleMap.IInfoWindowAdapter.GetInfoContents(Marker)'.
'MapRenderers.GetInfoContents(Marker)' cannot implement
'GoogleMap.IInfoWindowAdapter.GetInfoContents(Marker)' because it does
not have the matching return type of 'View'.".
and
The type or namespace name 'Views' does not exist in the namespace
'iVanApp.Android' (are you missing an assembly reference?)
Hope this did not break my code.
After investigating and reading about custom map issues (and mostly solutions) from others I got a workaround solution how to get pins on map but does not show info window when I click on pin(marker). Here I found workaround. If I modify OnMapReday and call&add method SetMapMarkers then pins are visible but as I said no info window is shown when I click on pin
protected override void OnMapReady(GoogleMap map)
{
base.OnMapReady(map);
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
SetMapMarkers();
}
private void SetMapMarkers()
{
NativeMap.Clear();
foreach (var pin in customPins)
{
NativeMap.AddMarker(CreateMarker(pin));
}
}
Although this is solution, I would prefer if I could get it to work without this workaround. Hence I did not put much effort why info window is not shown when you click on pin. In case there would be no solution without this workaround then I will be interested in solution with workaround but as I said I do not prefer to go this way.
As FreakyAli said that adding a custom pin and positions for CustomMap.
<local:CustomMap x:Name="customMap" MapType="Street" />
Adding a custom pin and positions the map's view with the MoveToRegion method
public MainPage()
{
InitializeComponent();
var pin = new CustomPin
{
Type = PinType.Place,
Position = new Position(37.79752, -122.40183),
Label = "Xamarin San Francisco Office",
Address = "394 Pacific Ave, San Francisco CA",
Id = "Xamarin",
Url = "http://xamarin.com/about/"
};
customMap.CustomPins = new List<CustomPin> { pin };
customMap.Pins.Add(pin);
customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(37.79752, -122.40183), Distance.FromMiles(1.0)));
}
I can get InfoWindow by clicking map pin, having no problem.
I am using a custom webview to convert my HTML data on the ios app. I am looking for the same custom renderer on android and windows.
MyWebView.cs
public class MyWebView : WebView
{
public static readonly BindableProperty UrlProperty = BindableProperty.Create(
propertyName: "Url",
returnType: typeof(string),
declaringType: typeof(MyWebView),
defaultValue: default(string));
public string Url
{
get { return (string)GetValue(UrlProperty); }
set { SetValue(UrlProperty, value); }
}
}
MyWebViewRenderer.cs on ios
[assembly: ExportRenderer(typeof(MyWebView), typeof(MyWebViewRenderer))]
namespace MyApp.iOS.Renderer
{
public class MyWebViewRenderer : ViewRenderer<MyWebView, WKWebView>
{
WKWebView _wkWebView;
protected override void OnElementChanged(ElementChangedEventArgs<MyWebView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
var config = new WKWebViewConfiguration();
_wkWebView = new WKWebView(Frame, config);
_wkWebView.NavigationDelegate = new MyNavigationDelegate();
SetNativeControl(_wkWebView);
}
}
public class MyNavigationDelegate : WKNavigationDelegate
{
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
string fontSize = "";
if (Device.Idiom == TargetIdiom.Phone)
{
fontSize = "250%";
}
else if (Device.Idiom == TargetIdiom.Tablet)
{
fontSize = "375%";
}
string stringsss = String.Format(#"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '{0}'", fontSize);
WKJavascriptEvaluationResult handler = (NSObject result, NSError err) =>
{
if (err != null)
{
System.Console.WriteLine(err);
}
if (result != null)
{
System.Console.WriteLine(result);
}
};
webView.EvaluateJavaScript(stringsss, handler);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Url")
{
Control.LoadHtmlString(Element.Url, null);
}
}
}
}
XAML and XAML.cs
<local:MyWebView
x:Name="web_view"
web_view.Url = "htmldata";
Output screenshot on ios device:
Sample HTML data added here. I need the same output on android and windows platforms, so requesting custom render codes for android and windows platforms.
If you want to present a string of HTML defined dynamically in code, you'll need to create an instance of HtmlWebViewSource:
var htmlSource = new HtmlWebViewSource();
htmlSource.Html = #"copy the html string here";
web_view.Source = htmlSource;
I'm using Picasso library to load image from URL to be rendered in a Google Map Cluster Marker, the problem is on the first load of the Fragment the images is not displaying I have to reload the Fragment for the images to display.
MapsFragment
private ClusterManager mClusterManager;
private ClusterManagerRenderer mClusterManagerRenderer;
private ArrayList<ClusterMarker> mClusterMarkers = new ArrayList<>();
//....
public void onMapReady(GoogleMap googleMap) {
if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) return;
//....
mMap = googleMap;
renderMarkers();
setGoogleMapStyle();
//.....
}
private void renderMarkers(){
//.....
if (mClusterManager != null && !mClusterMarkers.isEmpty()) {
mClusterManager.clearItems();
mClusterMarkers.clear();
}
if(mMap != null) {
if(mClusterManager == null) mClusterManager = new ClusterManager<ClusterMarker>(getActivity().getApplicationContext(), mMap);
if(mClusterManagerRenderer == null){
mClusterManagerRenderer = new ClusterManagerRenderer(
getContext(),
mMap,
mClusterManager
);
mClusterManager.setRenderer(mClusterManagerRenderer);
}
try{
ClusterMarker newClusterMarker = new ClusterMarker(
new LatLng((Double) eachImage.get("lat"), (Double) eachImage.get("lng")), // image lat lng
(String) eachImage.get("notes"), // marker title
(String) eachImage.get("notes"), // marker snippet
(String) eachImage.get("image") // image url http://i.imgur.com/DvpvklR.png
);
mClusterManager.addItem(newClusterMarker);
mClusterMarkers.add(newClusterMarker);
}catch (NullPointerException e){
Log.e("tag", "addMapMarkers: NullPointerException: " + e.getMessage() );
}
mClusterManager.cluster();
}
//....
Log.e("tag", "addMapMarkers: markers are set");
}
ClusterManagerRenderer
public class ClusterManagerRenderer extends DefaultClusterRenderer<ClusterMarker> {
//....
#Override
protected void onBeforeClusterItemRendered(ClusterMarker item, MarkerOptions markerOptions) {
Picasso.get().load(item.getThumbnail()).into(imageView);
Bitmap icon = iconGenerator.makeIcon();
markerOptions.icon(BitmapDescriptorFactory.fromBitmap(icon)).title(item.getTitle());
}
//....
}
I followed the Xamarin Tutorial Customizing a Map.
I changed it a bit to be able to draw a route.
public class CustomMap : Map
{
public static readonly BindableProperty RouteCoordinatesProperty =
BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);
public List<Position> RouteCoordinates
{
get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
set { SetValue(RouteCoordinatesProperty, value); }
}
}
The WinPhone 8.1 renderer:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace PROJECT.WinPhone
{
public class CustomMapRenderer : MapRenderer
{
MapControl nativeMap;
CustomMap formsMap;
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
nativeMap = Control as MapControl;
}
if (e.NewElement != null)
{
formsMap = (CustomMap)e.NewElement;
nativeMap = Control as MapControl;
//UpdatePolyLine();
var coordinates = new List<BasicGeoposition>();
foreach (var position in formsMap.RouteCoordinates)
{
coordinates.Add(new BasicGeoposition() { Latitude = position.Latitude, Longitude = position.Longitude });
}
var polyline = new MapPolyline();
polyline.StrokeColor = Windows.UI.Color.FromArgb(128, 255, 0, 0);
polyline.StrokeThickness = 5;
polyline.Path = new Geopath(coordinates);
nativeMap.MapElements.Add(polyline);
}
}
}
}
polyline.Path = new Geopath(coordinates); throws Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))
The problem is that my two others renderer (Android & iOS) works.. Maybe something isn't possible because I work with WinPhone8.1 unlike the tutorial, which is UWP
AndroidRenderer
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace PROJECT.Droid
{
public class CustomMapRenderer : MapRenderer, IOnMapReadyCallback
{
GoogleMap map;
Polyline polyline;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
}
if (e.NewElement != null)
{
((MapView)Control).GetMapAsync(this);
}
UpdatePolyLine();
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
if (map != null)
{
if (polyline != null)
{
polyline.Remove();
polyline.Dispose();
}
var polylineOptions = new PolylineOptions();
polylineOptions.InvokeColor(0x66FF0000);
int a = 0;
foreach (var position in ((CustomMap)this.Element).RouteCoordinates)
{
polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
}
polyline = map.AddPolyline(polylineOptions);
}
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
UpdatePolyLine();
}
}
}
iOSRenderer
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace PROJECT.iOS
{
public class CustomMapRenderer : MapRenderer
{
MKMapView nativeMap;
MKPolylineRenderer polylineRenderer;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
nativeMap = Control as MKMapView;
nativeMap.OverlayRenderer = null;
}
if (e.NewElement != null)
{
UpdatePolyLine();
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlay)
{
if (polylineRenderer == null)
{
polylineRenderer = new MKPolylineRenderer(overlay as MKPolyline);
polylineRenderer.FillColor = UIColor.Blue;
polylineRenderer.StrokeColor = UIColor.Red;
polylineRenderer.LineWidth = 3;
polylineRenderer.Alpha = 0.4f;
}
return polylineRenderer;
}
private void UpdatePolyLine()
{
if (nativeMap != null)
{
var formsMap = ((CustomMap)this.Element);
nativeMap = Control as MKMapView;
nativeMap.OverlayRenderer = GetOverlayRenderer;
CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[formsMap.RouteCoordinates.Count];
int index = 0;
foreach (var position in formsMap.RouteCoordinates)
{
coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
index++;
}
var routeOverlay = MKPolyline.FromCoordinates(coords);
nativeMap.AddOverlay(routeOverlay);
}
}
}
}
After a long time, I found why it didn't works...
If you're doing like me, if you do a HttpRequest to something as Google Direction API, then the result will comes after the first passage in the OnElementChanged().
Because formsMap.RouteCoordinates isn't null but empty, the Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED)) is thrown..
This is the good CustomMapRenderer for PolyLine use
using PROJECT;
using PROJECT.UWP;
using System.Collections.Generic;
using Windows.Devices.Geolocation;
using Windows.UI.Xaml.Controls.Maps;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.UWP;
using Xamarin.Forms.Platform.UWP;
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace PROJECT.UWP
{
public class CustomMapRenderer : MapRenderer
{
MapControl nativeMap;
CustomMap formsMap;
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
nativeMap = Control as MapControl;
}
if (e.NewElement != null)
{
formsMap = (CustomMap)e.NewElement;
nativeMap = Control as MapControl;
UpdatePolyLine();
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
{
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
if (formsMap != null && formsMap.RouteCoordinates.Count > 0)
{
List<BasicGeoposition> coordinates = new List<BasicGeoposition>();
foreach (var position in formsMap.RouteCoordinates)
{
coordinates.Add(new BasicGeoposition() { Latitude = position.Latitude, Longitude = position.Longitude });
}
Geopath path = new Geopath(coordinates);
MapPolyline polyline = new MapPolyline();
polyline.StrokeColor = Windows.UI.Color.FromArgb(128, 255, 0, 0);
polyline.StrokeThickness = 5;
polyline.Path = path;
nativeMap.MapElements.Add(polyline);
}
}
}
}
I am rendering the google maps for android
The first time I load a page with google maps it works, but when I close the page and reopen it, I get the exception:
System.NotSupportedException: Unable to activate instance of type SmartScout.Droid.CustomMapRenderer from native handle 0xcc700019 (key_handle 0x433fb7f8).
I see that the OnElementChanged in the MapRenderer is called twice and the second time it crashes with the exception above.
my code is:
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
return;
//map.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
//customPins = formsMap.CustomPins;
((MapView)Control).GetMapAsync(this);
}
}
Any solutions?
Change this
((MapView)Control).GetMapAsync(this);
and add this code
var androidMapView = (MapView)Control;
My full working code is following
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
var formsMap = (ExtendedMap)e.NewElement;
var androidMapView = (MapView)Control;
if (androidMapView != null && androidMapView.Map != null)
{
androidMapView.Map.InfoWindowClick += MapOnInfoWindowClick;
}
if (formsMap != null)
{
//code for run first time only
}
}