I have followed this link Xamarin.Forms WKWebView to inject Javascript into WebView for iOS. It has worked until Xamarin.Forms 3.3.
In Xamarin.Forms 3.3, the default custom renderer for iOS can be changed from UIWebView to WKWebView. I have followed the changes in AssemblyInfo.cs. Xamarin.Forms 3.3.0. Unfortunately, the changes break the codes.
Below are the changes
//protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
//if (Control == null)
//{
userController = new WKUserContentController();
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(this, "invokeAction");
//var config = new WKWebViewConfiguration { UserContentController = userController };
webView = NativeView as WKWebView;
webView.Configuration.UserContentController = userController;
webView.WeakUIDelegate = Self;
view = Element as BibleWebView;
//webView = (WKWebView)Control;
// var cgRect = new CoreGraphics.CGRect(view.X, view.Y, view.WidthRequest, view.HeightRequest);
// webView = new WKWebView(cgRect, config)
// {
// WeakUIDelegate = Self,
// };
// SetNativeControl(webView);
//}
//if (e.OldElement != null)
//{
// userController.RemoveAllUserScripts();
// userController.RemoveScriptMessageHandler("invokeAction");
// var hybridWebView = e.OldElement as BibleWebView;
// hybridWebView.Cleanup();
// e.OldElement.ShowPopup -= OnShowPopup;
//}
//if (Control != null)
//{
// BibleWebView webview = Element as BibleWebView;
HtmlWebViewSource htmlSource = (HtmlWebViewSource)view.Source;
string html = htmlSource.Html;
webView.LoadHtmlString(new NSString(html), NSBundle.MainBundle.ResourceUrl);
view.ShowPopup += OnShowPopup;
//}
}
The old codes are commented and the new codes are uncommented. Any help will be much appreciated.
I'm using a custom renderer with Xamarin.Forms 3.3 using EvaluateJavaScript to load javascript in a loaded page.
HybridWebView is a class in my shared project that inherit from Xamarin.Forms.WebView
First option
public class HybridWebViewRenderer : ViewRenderer<HybridWebView, WKWebView>
{
WKUserContentController userController;
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var formsWebView = sender as WebView;
if (formsWebView != null)
{
userController = new WKUserContentController();
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(Frame, config);
SetNativeControl(webView);
Control.AllowsBackForwardNavigationGestures = true;
Control.NavigationDelegate = new CustomWebViewClient(Element);
if((formsWebView.Source as UrlWebViewSource) != null)
{
string url = System.Web.HttpUtility.UrlPathEncode((formsWebView.Source as UrlWebViewSource).Url);
Control.LoadRequest(new NSUrlRequest(new NSUrl(url)));
}
else if((formsWebView.Source as HtmlWebViewSource) != null)
{
Control.LoadHtmlString((formsWebView.Source as HtmlWebViewSource).Html, new NSUrl(""));
}
}
}
public class CustomWebViewClient : WKNavigationDelegate, INSUrlConnectionDataDelegate
{
private HybridWebView _webclient;
private WKWebView _webView;
public CustomWebViewClient(HybridWebView webclient)
{
_webclient = webclient;
}
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
string allowZoom = #"javascript:
var div = document.createElement('div');
div.setAttribute('id', 'div1');
div.innerHTML = 'Test';
document.getElementsByClassName('container')[0].appendChild(div);
div.onclick = document.getElementById('div1').onclick = function() { div.innerHTML = 'Change text'; }";
webView.EvaluateJavaScript(allowZoom, null);
_webView = webView;
}
}
}
Second option
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var formsWebView = sender as WebView;
string allowZoom = #"javascript:
var div = document.createElement('div');
div.setAttribute('id', 'div1');
div.innerHTML = 'Test';
document.getElementsByClassName('container')[0].appendChild(div);
div.onclick = document.getElementById('div1').onclick = function() { div.innerHTML = 'Change text'; }";
if (formsWebView != null)
{
userController = new WKUserContentController();
//Using WKUserScript
var script = new WKUserScript(new NSString(allowZoom), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(Frame, config);
SetNativeControl(webView);
Control.AllowsBackForwardNavigationGestures = true;
Control.NavigationDelegate = new CustomWebViewClient(Element);
if((formsWebView.Source as UrlWebViewSource) != null)
{
string url = System.Web.HttpUtility.UrlPathEncode((formsWebView.Source as UrlWebViewSource).Url);
Control.LoadRequest(new NSUrlRequest(new NSUrl(url)));
}
else if((formsWebView.Source as HtmlWebViewSource) != null)
{
Control.LoadHtmlString((formsWebView.Source as HtmlWebViewSource).Html, new NSUrl(""));
}
}
}
Related
I'm using Xamarin.Forms.Map and I want to show pins on my map with already expanded window message(without click on them). Something like screenshot. By default window message show only after I clicked on them. How can I do this?
I did a test based on this sample:sample
The thing I do is override the GetViewForAnnotation method.
I add a subview and set it's position based on pin's position.
Here is relative code:
[assembly:ExportRenderer(typeof(CustomMap),typeof(CustomMapRenderer))]
namespace My_Forms_Test3.iOS
{
public class CustomMapRenderer:MapRenderer
{
UIView customPinView;
List<CustomPin> customPins;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
var nativeMap = Control as MKMapView;
nativeMap.GetViewForAnnotation = null;
nativeMap.CalloutAccessoryControlTapped -= OnCallourAccessoryControlTapped;
nativeMap.DidSelectAnnotationView -= OnDidSelect;
nativeMap.DidDeselectAnnotationView -= OnDidDeSelect;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
var nativeMap = Control as MKMapView;
customPins = formsMap.CustomPins;
nativeMap.GetViewForAnnotation = GetViewForAnnotation;
nativeMap.CalloutAccessoryControlTapped += OnCallourAccessoryControlTapped;
nativeMap.DidSelectAnnotationView += OnDidSelect;
nativeMap.DidDeselectAnnotationView += OnDidDeSelect;
}
}
private void OnDidDeSelect(object sender, MKAnnotationViewEventArgs e)
{
if (!e.View.Selected)
{
customPinView.RemoveFromSuperview();
customPinView.Dispose();
customPinView = null;
}
}
private void OnDidSelect(object sender, MKAnnotationViewEventArgs e)
{
throw new NotImplementedException();
}
private void OnCallourAccessoryControlTapped(object sender, MKMapViewAccessoryTappedEventArgs e)
{
throw new NotImplementedException();
}
protected override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
{
MKAnnotationView annotationView = null;
if (annotation is MKUserLocation)
return null;
var customPin = GetCustomPin(annotation as MKPointAnnotation);
if (customPin == null)
{
throw new Exception("not found");
}
annotationView = mapView.DequeueReusableAnnotation(customPin.Name);
if (annotationView == null)
{
annotationView = new CustomMKAnnotationView(annotation, customPin.Name);
annotationView.Image = UIImage.FromFile("pin.png");
annotationView.CalloutOffset = new CGPoint(0, 0);
annotationView.LeftCalloutAccessoryView = new UIImageView(UIImage.FromFile("monkey.png"));
annotationView.RightCalloutAccessoryView = UIButton.FromType(UIButtonType.DetailDisclosure);
((CustomMKAnnotationView)annotationView).Name = customPin.Name;
customPinView = new UIView();
var Label = new UILabel();
Label.Text = "Samsung";
Label.Frame=new CGRect(annotationView.GetFrame().X+35,annotationView.GetFrame().Y,100,50);
var Label2 = new UILabel();
Label2.Text = "20:20";
Label2.Frame = new CGRect(annotationView.GetFrame().X + 35, annotationView.GetFrame().Y+20, 100, 50);
customPinView.Frame= new CGRect(annotationView.GetFrame().X+40, annotationView.GetFrame().Y-20, 100, 50);
customPinView.AddSubview(Label);
customPinView.AddSubview(Label2);
Label.BaselineAdjustment = UIBaselineAdjustment.AlignBaselines;
customPinView.BackgroundColor = UIColor.White;
customPinView.Layer.CornerRadius = 5;
customPinView.Alpha = (nfloat)0.8;
customPinView.Layer.MasksToBounds = true;
annotationView.AddSubview(customPinView);
}
annotationView.CanShowCallout = true;
return annotationView;
}
CustomPin GetCustomPin(MKPointAnnotation annotation)
{
var position = new Position(annotation.Coordinate.Latitude, annotation.Coordinate.Longitude);
foreach (var pin in customPins)
{
if (pin.Position == position)
{ return pin; }
}
return null;
}}
result:
I have an Xamarin.Forms app that supports many languages. How do I show the Calender for the DatePicker with DatePicker Ok and Cancel button text in local language text from resource file?
My custom renderer
[assembly: ExportRenderer(typeof(CustomImageDatePicker), typeof(CustomImageDatePickerRenderer))]
namespace AMS.Droid.Renderers
{
public class CustomImageDatePickerRenderer : DatePickerRenderer
{
public CustomImageDatePickerRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e)
{
try
{
base.OnElementChanged(e);
CustomImageDatePicker element = (CustomImageDatePicker)this.Element;
if (Control == null)
return;
Control.Background = null;
if (Control != null && this.Element != null && !string.IsNullOrEmpty(element.Image))
{
Control.Background = AddPickerStyles(element.Image);
}
}
catch (Exception ex)
{
var message = ex.Message;
}
}
public LayerDrawable AddPickerStyles(string imagePath)
{
GradientDrawable gd = new GradientDrawable();
gd.SetColor(Android.Graphics.Color.Transparent);
gd.SetCornerRadius(25);
gd.SetStroke(3, Android.Graphics.Color.Black);
this.Control.SetBackgroundColor(Android.Graphics.Color.Transparent);
this.Control.SetPadding(20, 10, -50, 10);
Drawable[] layers = { gd, GetDrawable(imagePath) };
LayerDrawable layerDrawable = new LayerDrawable(layers);
layerDrawable.SetLayerInset(1, 0, 0, 30, 0);
return layerDrawable;
}
private BitmapDrawable GetDrawable(string imagePath)
{
try
{
int resID = Resources.GetIdentifier(imagePath.ToLower(), "drawable", this.Context.PackageName);
var drawable = ContextCompat.GetDrawable(this.Context, Resource.Drawable.brandIcon);
drawable.SetBounds(0, 0, (int)(drawable.IntrinsicWidth * 0.5),
(int)(drawable.IntrinsicHeight * 0.5));
var bitmap = ((BitmapDrawable)drawable).Bitmap;
var result = new BitmapDrawable(Resources, Bitmap.CreateScaledBitmap(bitmap, 60, 60, true));
result.Gravity = Android.Views.GravityFlags.Right;
//result.SetBounds(10, 10, 50, 0);
return result;
}
catch(Exception ex)
{
var message = ex.Message;
}
return null;
}`
Xaml:
<customDatePicker:CustomDatePicker
x:Name="dpFromDate"
DateSelected="FromDate_Selected"
Margin="10,5,10,0"
Image="brandIcon.png"/>
You could do this with custom renderer.
On Android, the DatePicker dialog can be customized by overriding the CreateDatePickerDialog method in a custom renderer.
[assembly: ExportRenderer(typeof(Xamarin.Forms.DatePicker), typeof(CustomDatePickerRenderer))]
namespace App10.Droid
{
public class CustomDatePickerRenderer : DatePickerRenderer
{
public CustomDatePickerRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.DatePicker> e)
{
base.OnElementChanged(e);
//Disposing
if (e.OldElement != null)
{
datePicker = null;
}
//Creating
if (e.NewElement != null)
{
datePicker = e.NewElement;
}
}
DatePickerDialog pickerDialog;
private Xamarin.Forms.DatePicker datePicker;
protected override DatePickerDialog CreateDatePickerDialog(int year, int month, int day)
{
pickerDialog = new DatePickerDialog(Context, (o, e) =>
{
datePicker.Date = e.Date;
((IElementController)datePicker).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false);
}, year, month, day);
//ok
pickerDialog.SetButton((int)DialogButtonType.Positive, "(FR)-DONE", OnDone);
//cancel
pickerDialog.SetButton((int)DialogButtonType.Negative, "(FR)-CLEAR", OnCancel);
return pickerDialog;
}
private void OnCancel(object sender, DialogClickEventArgs e)
{
datePicker.Unfocus();
}
private void OnDone(object sender, DialogClickEventArgs e)
{
datePicker.Date = ((DatePickerDialog)sender).DatePicker.DateTime;
datePicker.Unfocus();
}
}
}
You could replace the (FR)-DONE and (FR)-CLEAR with your own local language text from resource file
//ok
pickerDialog.SetButton((int)DialogButtonType.Positive, "(FR)-DONE", OnDone);
//cancel
pickerDialog.SetButton((int)DialogButtonType.Negative, "(FR)-CLEAR", OnCancel);
I have a created a custom Picker with downarrow image at right side using UITextFied in Xamarin ios. When I click the downarrow, the picker is not opening. But when the click centre of the UITextField, the picker is opening. How to open the pickerview when click of downarrow?
[assembly: ExportRenderer(typeof(CustomMonthPicker), typeof(CustomMonthPickerRenderer))]
namespace AMS.iOS.CustomRenderer
{
public class CustomMonthPickerRenderer : ViewRenderer<CustomMonthPicker, UITextField>
{
private DateTime _selectedDate;
private UITextField _dateLabel;
private PickerDateModel _pickerModel;
protected override void OnElementChanged(ElementChangedEventArgs<CustomMonthPicker> e)
{
try
{
base.OnElementChanged(e);
_dateLabel = new UITextField();
var dateToday = Element.Date;
SetupPicker(new DateTime(dateToday.Year, dateToday.Month, 1));
SetNativeControl(_dateLabel);
Control.EditingChanged += ControlOnEditingChanged;
Element.PropertyChanged += Element_PropertyChanged;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private void ControlOnEditingChanged(object sender, EventArgs e)
{
if (Element.Date.ToString().Equals(DateTime.MinValue.ToString()))
{
_dateLabel.Text = "";
}
else
{
var monthName = SetMonthNumberToMonthName(Element.Date.Month);
var currentDate = $"{monthName} | {Element.Date.Year}";
if (_dateLabel.Text != currentDate)
{
_dateLabel.Text = currentDate;
}
}
}
protected override void Dispose(bool disposing)
{
Element.PropertyChanged -= Element_PropertyChanged;
base.Dispose(disposing);
}
private void SetupPicker(DateTime date)
{
var datePicker = new UIPickerView();
_pickerModel = new PickerDateModel(datePicker, date);
datePicker.ShowSelectionIndicator = true;
_selectedDate = date;
_pickerModel.PickerChanged += (sender, e) =>
{
_selectedDate = e;
};
datePicker.Model = _pickerModel;
//_pickerModel.MaxDate = Element.MaxDate ?? DateTime.MaxValue;
//_pickerModel.MinDate = Element.MinDate ?? DateTime.MinValue;
var toolbar = new UIToolbar
{
BarStyle = UIBarStyle.Default,
Translucent = true
};
toolbar.SizeToFit();
var doneButton = new UIBarButtonItem("Done", UIBarButtonItemStyle.Done,
(s, e) =>
{
Element.Date = _selectedDate;
if (_selectedDate == DateTime.MinValue)
{
Element.Date = DateTime.Now;
}
var monthNameText = SetMonthNumberToMonthName(Element.Date.Month);
_dateLabel.Text = $"{monthNameText} | {Element.Date.Year}";
MessagingCenter.Send<App>((App)Xamarin.Forms.Application.Current, "PreferredDateChanged");
_dateLabel.ResignFirstResponder();
});
toolbar.SetItems(new[] { new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace), doneButton }, true);
Element.Date = _selectedDate;
var monthName = SetMonthNumberToMonthName(Element.Date.Month);
//if (Element.Date.Equals(DateTime.MinValue.ToString()))
//{
// _dateLabel.Text = "";
//}
//else
if (Element.Date.Year == 1)
{
_dateLabel.Text = "";
}
else
_dateLabel.Text = $"{monthName} | {Element.Date.Year}";
_dateLabel.InputAccessoryView = toolbar;
_dateLabel.TextColor = Element.TextColor.ToUIColor();
_dateLabel.VerticalAlignment = UIControlContentVerticalAlignment.Fill;
_dateLabel.HorizontalAlignment = UIControlContentHorizontalAlignment.Fill;
_dateLabel.TextAlignment = (UITextAlignment)TextAlignment.Center;
var downarrow = UIImage.FromBundle("brandIcon.png");
CGSize iconSize = downarrow.Size;
if (20 > -1)
iconSize = new CGSize((float)20, (float)20);
UIView paddingView = new UIView(new CGRect(0, 0, iconSize.Width + 8, iconSize.Height + 8));
UIImageView sideView = new UIImageView(new CGRect(0, 4, iconSize.Width, iconSize.Height));
sideView.Image = downarrow;
paddingView.AddSubview(sideView);
paddingView.UserInteractionEnabled = true;
_dateLabel.RightViewMode = UITextFieldViewMode.Always;
_dateLabel.RightView = paddingView;
//var gesture = new UITapGestureRecognizer(()=> {
// if (datePicker != null)
// {
// //datePicker.Hidden = !datePicker.Hidden;
// _dateLabel.InputView.Hidden = !_dateLabel.InputView.Hidden;
// //_dateLabel.AccessibilityRespondsToUserInteraction = true;
// }
//});
//paddingView.AddGestureRecognizer(gesture);
_dateLabel.RightView.UserInteractionEnabled = true;
// _dateLabel.RightView.AddGestureRecognizer(gesture);
_dateLabel.InputView = datePicker;
}
private void Element_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
try
{
if (e.PropertyName == CustomMonthPicker.MaxDateProperty.PropertyName)
{
_pickerModel.MaxDate = Element.MaxDate ?? DateTime.MinValue;
}
else if (e.PropertyName == CustomMonthPicker.MinDateProperty.PropertyName)
{
_pickerModel.MinDate = Element.MinDate ?? DateTime.MaxValue;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
How to open the pickerview when click of downarrow?
As ToolmakerSteve mentioned , we can add a tap gesture on the icon to focus the textfiled and it will open picker view automacially .
Try the following code
UIView paddingView = new UIView(new CGRect(0, 0, iconSize.Width + 8, iconSize.Height + 8));
UIImageView sideView = new UIImageView(new CGRect(0, 4, iconSize.Width, iconSize.Height));
sideView.Image = downarrow;
paddingView.AddSubview(sideView);
paddingView.UserInteractionEnabled = true;
_dateLabel.RightViewMode = UITextFieldViewMode.Always;
_dateLabel.RightView = paddingView;
//add this
sideView.UserInteractionEnabled = true;
UITapGestureRecognizer tap = new UITapGestureRecognizer(()=> {
_dateLabel.BecomeFirstResponder();
});
paddingView.AddGestureRecognizer(tap);
public MainPageView()
{
InitializeComponent();
MasterPage.NavMenuMListView.ItemSelected += OnItemSelected;
}
void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
MasterNavigationItem item = (MasterNavigationItem)e.SelectedItem;
if (item != null)
{
Detail = new NavigationPage((Page)Activator.CreateInstance(item.Target));
MasterPage.NavMenuMListView.SelectedItem = null;
IsPresented = false;
}
}
I have a master detail page(this is the code behind) and i want to respect mvvm conventions. this approach that is used in the docs uses the model inside the view. how can i make that onitemselected function in the viewmodel?
how can i make that onitemselected function in the viewmodel?
Download the source file from the link below. https://learn.microsoft.com/en-us/samples/xamarin/xamarin-forms-samples/navigation-masterdetailpage/
I make some changes with the source file. Use the command to binding for the listview with the ItemSelected event.
Install the package on NuGet.
Xamarin.Forms.BehaviorsPack: https://github.com/nuitsjp/Xamarin.Forms.BehaviorsPack
MasterPage.xaml
<ListView.Behaviors>
<behaviorsPack:SelectedItemBehavior Command="{Binding SelectedItemCommand}" />
</ListView.Behaviors>
MainPage.cs
public partial class MainPage : MasterDetailPage
{
public ICommand SelectedItemCommand { get; set; }
public MainPage()
{
InitializeComponent();
SelectedItemCommand = new Command(execute: () =>
{
var item = masterPage.listView.SelectedItem as MasterPageItem;
if (item != null)
{
Detail = new NavigationPage((Page)Activator.CreateInstance(item.TargetType));
masterPage.listView.SelectedItem = null;
IsPresented = false;
}
});
masterPage.BindingContext = this;
//masterPage.listView.ItemSelected += OnItemSelected;
//if (Device.RuntimePlatform == Device.UWP)
//{
// MasterBehavior = MasterBehavior.Popover;
//}
}
//void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
//{
// var item = e.SelectedItem as MasterPageItem;
// if (item != null)
// {
// Detail = new NavigationPage((Page)Activator.CreateInstance(item.TargetType));
// masterPage.listView.SelectedItem = null;
// IsPresented = false;
// }
//}
}
In one of the apps I'm working on I require the use of custom map pins and I've followed the guide on Xamarin https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/map/customized-pin/ as well as borrowed their sample code to try and make my own example.
It works to a degree in such that the info window is actually updated to the custom layout but the map pin never changes.
My CustomMapRenderer:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Android.Widget;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.Android;
using WorkingWithMaps.Droid.Renderers;
using WorkingWithMaps;
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace WorkingWithMaps.Droid.Renderers
{
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;
((MapView)Control).GetMapAsync(this);
}
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
map = googleMap;
map.SetInfoWindowAdapter(this);
map.InfoWindowClick += OnInfoWindowClick;
this.NativeMap = googleMap;
}
protected override void OnElementPropertyChanged(object sender, 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.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);
}
}
public Android.Views.View GetInfoContents(Marker marker)
{
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
Android.Views.View view;
var customPin = GetCustomPin(marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (customPin.Id == "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 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;
}
}
}
and my map page, also heavily borrowed from Xamarin's working with maps guide
using Plugin.Geolocator;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Xaml;
namespace WorkingWithMaps
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
CustomMap map;
Geocoder geoCoder;
String navAdd;
public MainPage()
{
InitializeComponent();
var maplocator = CrossGeolocator.Current;
maplocator.DesiredAccuracy = 1;
geoCoder = new Geocoder();
map = new CustomMap
{
HeightRequest = 100,
WidthRequest = 960,
VerticalOptions = LayoutOptions.FillAndExpand,
IsShowingUser = true
};
map.MapType = MapType.Street;
map.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(55.237208, 10.479160), Distance.FromMeters(500)));
map.IsShowingUser = true;
var street = new Button { Text = "Street" };
var hybrid = new Button { Text = "Hybrid" };
var satellite = new Button { Text = "Satellite" };
street.Clicked += HandleClickedAsync;
hybrid.Clicked += HandleClickedAsync;
//satellite.Clicked += OnReverseGeocodeButtonClicked;
var segments = new StackLayout
{
Spacing = 30,
HorizontalOptions = LayoutOptions.CenterAndExpand,
Orientation = StackOrientation.Horizontal,
Children = { street, hybrid, satellite }
};
Content = new StackLayout
{
HorizontalOptions = LayoutOptions.Center,
Children = { map, segments }
};
Device.BeginInvokeOnMainThread(async () =>
{
try
{
//var currentpos = await maplocator.GetPositionAsync(1000);
//map.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(currentpos.Latitude, currentpos.Longitude), Distance.FromMeters(500)));
if (!maplocator.IsListening)
{
await maplocator.StartListeningAsync(1000, 50, true);
}
}
catch (Exception ex)
{
Debug.WriteLine("Fail" + ex);
}
});
var pin = new CustomPin
{
Pin = new Pin
{
Type = PinType.Place,
Position = new Position(55.240121, 10.469895),
Label = "Testing Pins"
}
};
map.CustomPins = new List<CustomPin> { pin };
map.Pins.Add(pin.Pin);
map.PropertyChanged += (sender, e) =>
{
Debug.WriteLine(e.PropertyName + " just changed!");
if (e.PropertyName == "VisibleRegion" && map.VisibleRegion != null)
CalculateBoundingCoordinates(map.VisibleRegion);
};
maplocator.PositionChanged += (sender, e) =>
{
var position = e.Position;
map.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(position.Latitude, position.Longitude), Distance.FromKilometers(2)));
};
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
//async void OnReverseGeocodeButtonClicked(object sender, EventArgs e)
//{
// var possibleAddresses = await geoCoder.GetAddressesForPositionAsync(pin.Position);
// navAdd += possibleAddresses.ElementAt(0) + "\n";
// switch (Device.OS)
// {
// case TargetPlatform.iOS:
// Device.OpenUri(new Uri(string.Format("http://maps.apple.com/?q={0}", WebUtility.UrlEncode(navAdd))));
// break;
// case TargetPlatform.Android:
// Device.OpenUri(new Uri(string.Format("geo:0,0?q={0}", WebUtility.UrlEncode(navAdd))));
// break;
// case TargetPlatform.Windows:
// case TargetPlatform.WinPhone:
// Device.OpenUri(new Uri(string.Format("bingmaps:?where={0}", Uri.EscapeDataString(navAdd))));
// break;
// }
//}
void HandleClickedAsync(object sender, EventArgs e)
{
var b = sender as Button;
switch (b.Text)
{
case "Street":
map.MapType = MapType.Street;
break;
case "Hybrid":
map.MapType = MapType.Hybrid;
break;
case "Satellite":
map.MapType = MapType.Satellite;
break;
}
}
static void CalculateBoundingCoordinates(MapSpan region)
{
var center = region.Center;
var halfheightDegrees = region.LatitudeDegrees / 2;
var halfwidthDegrees = region.LongitudeDegrees / 2;
var left = center.Longitude - halfwidthDegrees;
var right = center.Longitude + halfwidthDegrees;
var top = center.Latitude + halfheightDegrees;
var bottom = center.Latitude - halfheightDegrees;
if (left < -180) left = 180 + (180 + left);
if (right > 180) right = (right - 180) - 180;
Debug.WriteLine("Bounding box:");
Debug.WriteLine(" " + top);
Debug.WriteLine(" " + left + " " + right);
Debug.WriteLine(" " + bottom);
}
}
}
On top of the mentioned issue the implementation has also caused IsShowingUser = True to no longer function as well as
var currentpos = await maplocator.GetPositionAsync(1000);
to throw an exception.
Github repository: https://github.com/Mortp/CustomMapPinsXamarin
First of all I would like to provide 2 links that helped me to understand the problem. Thank you guys.
Xamarin.Forms.Maps 2.3.4 custom MapRenderer disables everything and https://forums.xamarin.com/discussion/92565/android-ionmapreadycallback-forms-2-3-4
Latest Xamarin Maps broke OnElementPropertyChanged with VisibleRegion. They defined that MapRenderer now implements IOnMapReadyCallback and that broke somehow OnElementPropertyChanged (I didn't investigate how and why). As you can see in link I provided there are 2 methods you can go. To keep your renderer implementing IOnMapReadyCallback or not. When I kept IOnMapReadyCallback I started to get 2 pins - one of top another - our custom pin and regular pin. I didn't dig more how that happens and removed IOnMapReadyCallback. After that things became really simple because if you let Xamarin handle it and create NativeMap you can remove some code and make renderer simpler.
Before I post the code I also want to mention that when I fixed it the app started crashing with OutOfMemory exception and I found out that your pin image is 2000 pixels width. I changed it to 40. Below is the code:
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter//, IOnMapReadyCallback
{
bool isDrawn;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
NativeMap.InfoWindowClick -= OnInfoWindowClick;
}
}
bool isMapReady;
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (!isMapReady && (NativeMap != null))
{
NativeMap.SetInfoWindowAdapter(this);
NativeMap.InfoWindowClick += OnInfoWindowClick;
isMapReady = true;
}
if (e.PropertyName.Equals("VisibleRegion") && !isDrawn)
{
NativeMap.Clear();
foreach (var pin in ((CustomMap)Element).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.pin));
NativeMap.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);
}
}
public Android.Views.View GetInfoContents(Marker marker)
{
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
Android.Views.View view;
var customPin = GetCustomPin(marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (customPin.Id == "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 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 ((CustomMap)Element).CustomPins)
{
if (pin.Pin.Position == position)
{
return pin;
}
}
return null;
}
}