Xamarin Forms - Show Maps Info windows like Uber App - c#

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;
}
}
}

Related

Xamarin Forms custom control: implement a method in the renderer

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);
}

How to inject Javascript at Document Start using C# on Xamarin Android

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!

Keyboard in front of my interface

I've many Entries and Labels in my Interface, what can I do to my page scrolldown if my keyboard is in front of any element? I tried to implement a scrollview in my xaml but no results...
I had a similar issue.
Best way to resolve this was to wrap the login part into a scrollview (similar to what you are doing already) and using custom renderers:
Android:
[assembly: ExportRenderer(typeof(ModernLogin), typeof(ModernLoginPageRenderer))]
namespace MyApp.Droid.Renderer
{
public class ModernLoginPageRenderer : PageRenderer
{
public ModernLoginPageRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
// Deactivate SoftInput View Adjustment for this window
if (e.NewElement != null)
{
(this.Context as FormsApplicationActivity).Window.SetSoftInputMode(SoftInput.AdjustResize);
}
}
protected override void OnWindowVisibilityChanged([GeneratedEnum] ViewStates visibility)
{
// Enable SoftInput View Adjustment after moving away from this window
if (visibility == ViewStates.Gone)
{
(this.Context as FormsApplicationActivity).Window.SetSoftInputMode(SoftInput.AdjustPan);
}
base.OnWindowVisibilityChanged(visibility);
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
base.OnLayout(changed, left, top, right, bottom);
}
}
}
iOS:
[assembly: ExportRenderer(typeof(ModernLogin), typeof(ModernLoginPageRenderer))]
namespace MyApp.iOS.Renderer
{
public class ModernLoginPageRenderer : PageRenderer
{
NSObject observerHideKeyboard;
NSObject observerShowKeyboard;
public override void ViewDidLoad()
{
base.ViewDidLoad();
var cp = Element as ModernLogin;
if (cp != null)
{
foreach (var g in View.GestureRecognizers)
{
g.CancelsTouchesInView = true;
}
}
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
observerHideKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardNotification);
observerShowKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardNotification);
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
NSNotificationCenter.DefaultCenter.RemoveObserver(observerHideKeyboard);
NSNotificationCenter.DefaultCenter.RemoveObserver(observerShowKeyboard);
}
void OnKeyboardNotification(NSNotification notification)
{
if (!IsViewLoaded) return;
var frameBegin = UIKeyboard.FrameBeginFromNotification(notification);
var frameEnd = UIKeyboard.FrameEndFromNotification(notification);
var page = Element as ModernLogin;
if (page != null)
{
var padding = page.Padding;
page.Padding = new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + frameBegin.Top - frameEnd.Top);
}
}
}

Change button style programmatically

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;
}
}

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
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 });
}
}
}
}

Categories

Resources