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
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
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 });
}
}
}
}
Related
Please assist
I have been struggling in showing a trip on my carpooling app much like the one on an Uber. That is with two info windows for both start and end locations open with their respective addresses.
I have managed to render a custom info window the way I want to but can not make all the info windows open. How can I do one of the following:
Override the current logic that prevents google Maps to open more than one info window at a time.
Draw over the map to display a custom view right next to the Map pin.
I am using 'Xamarin.Forms.GoogleMaps'
Visual goal(Only top part of screen)
Xamarin.Android Custom Render Class
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace CoTripApp.Droid.CustomRennders
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter
{
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//nativemap.infowindowclick -= oninfowindowclick;
}
if (e.NewElement != null)
{
var formsmap = (CustomMap)e.NewElement;
}
}
protected override void OnMapReady(GoogleMap nativeMap, Xamarin.Forms.GoogleMaps.Map map)
{
base.OnMapReady(nativeMap, map);
NativeMap.SetInfoWindowAdapter(this);
}
protected override void OnMarkerCreated(Pin pin, Marker marker)
{
marker.Position = new LatLng(pin.Position.Latitude, pin.Position.Longitude);
marker.Title = pin.Label;
marker.Snippet = pin.Address;
if (pin.Type.Equals(PinType.Generic))
{
marker.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(Resource.Drawable.circle));
}
else
{
marker.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(Resource.Drawable.circle_closed));
}
marker.ShowInfoWindow();
}
public Android.Views.View GetInfoContents(Marker marker)
{
Android.Views.View view;
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
view = inflater.Inflate(Resource.Layout.CustomMapInfoWindow, null);
view.FindViewById<TextView>(Resource.Id.waypointNo).Text = marker.Title;
view.FindViewById<TextView>(Resource.Id.address).Text = marker.Snippet;
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
}
}
So i do some validations on my entries and i would like to change the color to red when the value is invalid.
Is there a way to do this in xamarin right now ?
I know that there's renderer to change the color permanently but i just want to change it based on a condition and leave it black when everything is ok.
Thank you.
You could overwrite OnElementPropertyChanged method in your CustomRenderer to achieve this.
For example:
for Android:
[assembly: ExportRenderer(typeof(Entry), typeof(UnderLineEntry))]
namespace EntryCa.Droid
{
class UnderLineEntry : EntryRenderer
{
public UnderLineEntry(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control == null || e.NewElement == null) return;
Control.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Black);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Entry.TextProperty.PropertyName)
{
if (Control.Text.Length > 6) //this is your condition(For example, here is the length of the text content)
{
Control.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Red);
}
else
{
Control.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Black);
}
}
}
}
}
the ios is the similar to Android,also change it in the OnElementPropertyChanged method,if you want i could give you an example.
for ios.
[assembly: ExportRenderer(typeof(Entry), typeof(MyEntryRenderer))]
namespace EntryCa.iOS
{
public class MyEntryRenderer : EntryRenderer
{
private CALayer _line;
public override void LayoutSubviews()
{
base.LayoutSubviews();
Control.BorderStyle = UITextBorderStyle.None;
_line = new CALayer
{
BackgroundColor = UIColor.Black.CGColor,
Frame = new CGRect(0, Frame.Height, Frame.Width, 1f)
};
Control.Layer.AddSublayer(_line);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Entry.TextProperty.PropertyName)
{
if (Control.Text.Length > 6)
{
_line.RemoveFromSuperLayer();
_line = new CALayer
{
BackgroundColor = UIColor.Red.CGColor,
Frame = new CGRect(0, Frame.Height, Frame.Width, 1f)
};
Control.Layer.AddSublayer(_line);
}
else
{
_line.RemoveFromSuperLayer();
_line = new CALayer
{
BackgroundColor = UIColor.Black.CGColor,
Frame = new CGRect(0, Frame.Height, Frame.Width, 1f)
};
Control.Layer.AddSublayer(_line);
}
}
}
}
}
To change the line, have to use a custom renderer like this.
Or just hide the line in the custom renderer and fake a line on the page, then you can control the color by Data trigger.
Alternatively, what about adding a frame or changing the background color instead of the entry.
I'm trying to create an extended version of the standard XF Map object:
public class RRMap: Map
{
public void DoSomethingOnMap() {
/* ... */
}
}
I also created an Android renderer (iOS will come later):
[assembly: ExportRenderer(typeof(RRMap), typeof(RRMapRendererAndroid))]
namespace MyApp.Droid.Renderers
{
public class RRMapRendererAndroid : MapRenderer
{
public RRMapRendererAndroid(Context context) : base(context) { }
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.GetMapAsync(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.DefaultMarker(210));
return marker;
}
}
}
Everything is working fine so far: the map is rendered and pins are created with a custom color.
Unfortunately, I'm stuck on the implementation of DoSomethingOnMap method: it should be a method in the shared code, but it should be implemented in different ways, depending on the platform.
In other circumstances, I would create an interface using DependencyService for implementation, but in this particular case I can't figure out how to proceed.
The first solution is you can use a messaging-center, this can communicate between shared project and iOS/Android project.
Publish a message in the doSomethingOnMap method and anywhere you subscribed to the message will be triggered.
The second is create an event in your shared project and subscribe to that event in the renderer, I wrote both two solutions below:
In your shared project:
public class CustomMap : Map
{
public List<CustomPin> CustomPins { get; set; }
public event EventHandler CallToNativeMethod;
public void doSomething()
{
if (CallToNativeMethod != null)
CallToNativeMethod(this, new EventArgs());
}
public void doSomething(CustomMap myMap) {
MessagingCenter.Send<CustomMap>(this, "Hi");
}
}
In the renderer:
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
}
if (e.NewElement != null)
{
MessagingCenter.Subscribe<CustomMap>(this, "Hi", (sender) =>
{
// Do something whenever the "Hi" message is received
Console.WriteLine("hi");
});
((CustomMap)e.NewElement).CallToNativeMethod += (sender, arg) =>
{
Console.WriteLine("native method");
};
}
}
At anywhere you want to call this method:
private void Button_Clicked(object sender, System.EventArgs e)
{
customMap.doSomething();
customMap.doSomething(customMap);
}
I'm trying to port an app from Swift (iOS only) to C# in Visual Studio - and it's going (slightly) well. I'm having some Android troubles though (many of them - but only one for this question!)
The page loads correctly in the webview of the Android version of the app - but the Javascript doesn't execute until after the page has rendered, the result being that the App Store advert is displayed briefly before it disappears.
My iOS app works correctly - the source code for the iOS version is here:
public class PortalViewRenderer : ViewRenderer<PortalView, WKWebView>, IWKScriptMessageHandler, IWKNavigationDelegate {
private class NavigationDelegate : WKNavigationDelegate {
private readonly WeakReference<PortalViewRenderer> _webView;
public NavigationDelegate(PortalViewRenderer webView) {
_webView = new WeakReference<PortalViewRenderer>(webView);
}
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) {
}
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation) {
NSUrl currentURL = webView.Url;
var current = Connectivity.NetworkAccess;
if (current != NetworkAccess.Internet) {
if (!(currentURL.AbsoluteString.Contains("file://"))) {
string noConnectionPath = Path.Combine(NSBundle.MainBundle.BundlePath, "Common/NoInternet.html");
webView.LoadRequest(new NSUrlRequest(NSUrl.FromString(noConnectionPath)));
}
}
}
public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) {
}
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler) {
NSUrl url = navigationAction.Request.Url;
if (url != null) {
if (url.Host == "<website url>" || url.AbsoluteString.Contains("file://")) {
decisionHandler(WKNavigationActionPolicy.Allow);
} else if (url.AbsoluteString.Contains("<website url>/mydavylamp/timeout")) {
decisionHandler(WKNavigationActionPolicy.Cancel);
webView.LoadRequest(new NSUrlRequest(NSUrl.FromString(Element.Uri)));
} else {
UIApplication.SharedApplication.OpenUrl(url);
}
}
}
}
WKUserContentController userController;
protected override void OnElementChanged (ElementChangedEventArgs<PortalView> e) {
base.OnElementChanged (e);
var javaScriptFunction = System.IO.File.ReadAllText("Common/HideAppStoreAds.js");
if (Control == null) {
userController = new WKUserContentController();
var script = new WKUserScript(new NSString(javaScriptFunction), WKUserScriptInjectionTime.AtDocumentStart, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(this, "invokeAction");
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(Frame, config);
webView.BackgroundColor = UIKit.UIColor.FromRGB(0x11, 0x25, 0x43);
webView.ScrollView.BackgroundColor = webView.BackgroundColor;
webView.CustomUserAgent = "headbanger.davylamp.ios";
webView.ScrollView.ScrollEnabled = true;
webView.ScrollView.Bounces = false;
webView.AllowsBackForwardNavigationGestures = false;
webView.ContentMode = UIKit.UIViewContentMode.ScaleToFill;
webView.NavigationDelegate = new NavigationDelegate(this);
SetNativeControl(webView);
}
if (e.OldElement != null) {
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
var portalView = e.OldElement as PortalView;
portalView.Cleanup();
}
if (e.NewElement != null) {
Control.LoadRequest(new NSUrlRequest(NSUrl.FromString(Element.Uri)));
}
}
public void DidReceiveScriptMessage (WKUserContentController userContentController, WKScriptMessage message) {
Element.InvokeAction (message.Body.ToString ());
}
}
The Android version is as follows (it doesn't do so much yet, because I haven't worked out how to set policies etc. yet) but my main concern is that I can't get the Javascript to run at the correct time (as set on the iOS version using WKUserScriptInjectionTime which doesn't seem to have an Android equivalent)
public class JavascriptWebViewClient : WebViewClient {
string _javascript;
public JavascriptWebViewClient(string javascript) {
_javascript = javascript;
}
public override void OnPageFinished(WebView view, string url) {
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
}
}
public class PortalViewRenderer : ViewRenderer<PortalView, Android.Webkit.WebView> {
Context _context;
public PortalViewRenderer(Context context) : base(context) {
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<PortalView> e) {
string javascriptFunction;
Android.Content.Res.AssetManager assets = _context.Assets;
using (StreamReader sr = new StreamReader(assets.Open("Common/HideAppStoreAds.js"))) {
javascriptFunction = sr.ReadToEnd();
}
base.OnElementChanged(e);
if (Control == null) {
var webView = new Android.Webkit.WebView(_context);
webView.Settings.JavaScriptEnabled = true;
webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {javascriptFunction}"));
SetNativeControl(webView);
}
if (e.OldElement != null) {
Control.RemoveJavascriptInterface("jsBridge");
var portalView = e.OldElement as PortalView;
portalView.Cleanup();
}
if (e.NewElement != null) {
Control.LoadUrl($"{Element.Uri}");
}
}
}
The (common) Javascript is as follows:
var styleTag = document.createElement("style");
styleTag.textContent = '.mobile-apps {display:none;}';
document.documentElement.appendChild(styleTag);
I've googled for the magic spell, but I can't seem to find any guides on how to build a webview for Android in C# - and particularly not for iOS developers!
As always, any help that anyone can provide will be gratefully received.
The answer, for my use case, seems to be as follows.
The Android documentation (https://developer.android.com/reference/android/webkit/WebViewClient) for onPageCommitVisible says that:
This callback can be used to determine the point at which it is safe to make a recycled WebView visible, ensuring that no stale content is shown. It is called at the earliest point at which it can be guaranteed that WebView#onDraw will no longer draw any content from previous navigations. The next draw will display either the WebView#setBackgroundColor of the WebView, or some of the contents of the newly loaded page.
To my mind, this means that the HTML has loaded (although not necessarily any other resources) and the page might start to be rendered (although, crucially, it won't be rendered until this callback completes.)
I used the following code:
public override void OnPageCommitVisible(WebView view, string url) {
view.EvaluateJavascript(_javascript, null);
base.OnPageCommitVisible(view, url);
}
and it seems to work correctly. I hope that this helps anyone else.
You need just to override OnPageCommitVisible method:
public override void OnPageCommitVisible(AWebView view, string url)
{
//Insert javascript injection first
view.EvaluateJavascript(_javascript, null);
base.OnPageCommitVisible(view, url);
}
It fires Certainly.
Have a good code!
I want to draw material design Flat Button on android, for that i made this renderer:
public class NativeFlatButtonRenderer : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
var oldElement = e.OldElement as NativeFlatButton;
if (oldElement != null)
{
}
var newElement = e.NewElement as NativeFlatButton;
if (newElement != null)
{
Control.SetBackgroundResource(Android.Resource.Style.WidgetMaterialButtonBorderless);
}
}
}
and it throws Android.Content.Res.Resources+NotFoundException: Resource ID #0x1030259
when i change to
if (newElement != null)
{
var button = new AppCompatButton(Context, null, Android.Resource.Style.WidgetMaterialButtonBorderlessColored);
SetNativeControl(button);
}
its drawing but without commands etc. So how i can draw Flat Button with renderer on android?
That was quiet easy. Here is the ButtonRenderer method that creates native control:
protected override Android.Widget.Button CreateNativeControl()
{
return new Android.Widget.Button(this.Context);
}
So i only need to override it and there is my complete renderer:
public class NativeFlatButtonRenderer : ButtonRenderer
{
protected override Android.Widget.Button CreateNativeControl()
{
var button = new AppCompatButton(Context, null, Android.Resource.Style.WidgetMaterialButtonBorderlessColored);
return button;
}
}