I have used the following code, but it is not working.
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
using XLabs.Platform.Device;
using XLabs.Platform.Services.Media;
namespace CalbrenEnterprises.Pages
{
public class TestPage : ContentPage
{
private ImageSource imageSource;
private IMediaPicker mediaPicker;
private Image img;
private string status;
public TestPage()
{
this.Title = "Camera Test";
NavigationPage.SetHasNavigationBar(this, false);
img = new Image() { HeightRequest = 300, WidthRequest = 300, BackgroundColor = Color.FromHex("#D6D6D2"), Aspect = Aspect.AspectFit };
var addPictureButton = new Button()
{
Text = "Select Picture",
Command = new Command(async () => { await SelectPicture(); })
};
StackLayout stack = new StackLayout();
stack.VerticalOptions = LayoutOptions.FillAndExpand;
stack.Children.Add(new BoxView { Color = Color.Transparent, HeightRequest = 20 });
stack.Children.Add(addPictureButton);
stack.Children.Add(img);
ScrollView scrollview = new ScrollView
{
Orientation = ScrollOrientation.Vertical,
VerticalOptions = LayoutOptions.FillAndExpand,
Content = stack
};
this.Content = new StackLayout
{
Children = { scrollview }
};
}
private async Task SelectPicture()
{
mediaPicker = DependencyService.Get<IMediaPicker>();
imageSource = null;
try
{
var mediaFile = await mediaPicker.SelectPhotoAsync(new CameraMediaStorageOptions
{
DefaultCamera = CameraDevice.Front,
MaxPixelDimension = 400
});
imageSource = ImageSource.FromStream(() => mediaFile.Source);
img.Source = imageSource;
}
catch (System.Exception ex)
{
this.status = ex.Message;
}
}
}
}
Question:
How can I select photos from gallery and take photos from camera in PCL project in Xamarin.forms?
var device = Resolver.Resolve<IDevice>();
mediaPicker = DependencyService.Get<IMediaPicker>() ?? device.MediaPicker;
if (mediaPicker == null) throw new NullReferenceException("MediaPicker DependencyService.Get error");
try
{
if (mediaPicker.IsCameraAvailable)
{
var options = new CameraMediaStorageOptions() {
DefaultCamera = CameraDevice.Front,
SaveMediaOnCapture = true,
Directory = "YourAppName",
Name = string.Format("YourAppName_{0}", DateTime.Now.ToString("yyMMddhhmmss")),
MaxPixelDimension = 1024,
PercentQuality = 85
};
var mediaFile = await mediaPicker.TakePhotoAsync(options);
if (mediaFile != null && mediaFile.Source != null)
{
// do something with your photo
}
}
else
{
Logger.Info("Camera not available");
}
}
catch (TaskCanceledException)
{
Logger.Info("TakePhoto cancelled");
}
catch (Exception ex)
{
Logger.Error(ex);
}
Related
I am creating a cross platform school app. I have a page that displays the students in a class and their pictures. I added a functionality that enables the teacher to change the profile of the student or add one if none. the thing is that this functionality works great on android in which they can change or add multiple profiles for different students at once. but on ios, something seems wrong. If they add multiple pics, the pictures won't be uploaded to my database and thus no pics are displayed, but if they add only one pic for a student, it will be uploaded.
Here is my code:
private void generateProfiles(double width, List<studentdetailsformanagment> lstofstudents,List<absenteesIds>attending)
{
studentprofiles.Children.Clear();
dtAttedndance.Clear();
double nbofcolumns = width / 180;
double nbofrows = lstofstudents.Count / nbofcolumns;
for (int i = 0; i < nbofcolumns-1; i++)
{
studentprofiles.ColumnDefinitions.Add(new ColumnDefinition { Width = 170 });
}
for (int j = 0; j < nbofrows + 1; j++)
{
studentprofiles.RowDefinitions.Add(new RowDefinition { Height = 170 });
}
for (int i = 0; i < lstofstudents.Count; i++)
{
var student = lstofstudents[i];
DataRow dtdr = dt.Select("[id] ='" + student.id + "'").FirstOrDefault();
if (dtdr == null)
dt.Rows.Add(Preferences.Get("AcademicYearId", ""),student.id, section.Text, student.profilePic);
SwipeItem delete = new SwipeItem
{
Text = "Remove",
BackgroundColor = Color.Transparent
};
SwipeItems items = new SwipeItems
{
Mode = SwipeMode.Execute
};
items.Add(delete);
List<SwipeItem> swipeItems = new List<SwipeItem>() { delete };
Image img = new Image
{
Source = ImageSource.FromResource("ALNahrainAlphaApp.icons.user_profile.png"),
Aspect = Aspect.AspectFill,
Margin = -20,
HeightRequest = 100,
WidthRequest = 100,
};
if (student.profilePic == null)
img.Source = ImageSource.FromResource("ALNahrainAlphaApp.icons.user_profile.png");
else
{
byte[] bytes = System.Convert.FromBase64String(student.profilePic);
img.Source = ImageSource.FromStream(() => new MemoryStream(bytes));
}
StackLayout stdinfo = new StackLayout { Orientation = StackOrientation.Vertical, ClassId="false" };
if (attending.Count > 0)
{
for (int j = 0; j < attending.Count; j++)
{
if (student.id == attending[j].id)
{
stdinfo.BackgroundColor = Color.FromHex("#0d98ba");
stdinfo.ClassId = "true";
}
}
}
Label stdname = new Label { TextColor = Color.Black, Text = student.name, VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center };
Frame circleImageFrame = new Frame
{
ClassId = student.id,
Margin = 10,
BorderColor = Color.Black,
CornerRadius = 50,
HeightRequest = 60,
WidthRequest = 60,
IsClippedToBounds = true,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
Content = img
};
stdinfo.Children.Add(circleImageFrame);
stdinfo.Children.Add(stdname);
SwipeView swipe = new SwipeView
{
Threshold=70,
LeftItems = new SwipeItems(items),
Content = stdinfo,
};
AddTap.Tapped += async (s, evt) =>
{
try
{
await PickAndShow(null, img);
byte[] profilepic = System.IO.File.ReadAllBytes(imgpath);
string imageBase64 = Convert.ToBase64String(profilepic);
try
{
DataRow dr = dt.Select("[id] ='" + circleImageFrame.ClassId + "'").FirstOrDefault();
if (dr != null) dr["profilePic"] = imageBase64;
var obj = stdprofiles.FirstOrDefault(x => x.id == circleImageFrame.ClassId);
if (obj != null) obj.profilePic = imageBase64;
}
catch (Exception exp)
{
await DisplayAlert("Error", exp.Message, "ok");
}
}
catch(Exception exp)
{
}
};
circleImageFrame.GestureRecognizers.Add(AddTap);
this is the function i use to display circular frames with profiles.
async Task<FileResult> PickAndShow(PickOptions options, Image image)
{
imgpath = "";
try
{
var result = await FilePicker.PickAsync(options);
if (result != null)
{
Text = $"File Name: {result.FileName}";
if (result.FileName.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) ||
result.FileName.EndsWith("png", StringComparison.OrdinalIgnoreCase))
{
stream = await result.OpenReadAsync();
image.Source = ImageSource.FromStream(() => stream);
}
}
imgpath = result.FullPath;
return result;
}
catch (Exception ex)
{
}
return null;
}
and this is the function i use to pick an image from the folders in the device
this is the code that runs when i press submit:
async private void submit_Clicked(object sender, EventArgs e)
{
submit.IsEnabled = false;
if (flagEditorAttednd == true)
{
flagEditorAttednd = false;
edit.Source = ImageSource.FromResource("ALNahrainAlphaApp.icons.edit.png");
select.IsEnabled = false;
urlClass urldata = new urlClass();
string uri = urldata.url + "/PostStdDetails";
StringContent content = new StringContent(JsonConvert.SerializeObject(dt), Encoding.UTF8, "application/json");
try
{
HttpResponseMessage responsepost = await client.PostAsync(uri, content);
if (responsepost.IsSuccessStatusCode == true)
{
string outcome = await responsepost.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<string>(outcome);
await DisplayAlert("Students Distribution", "Students in section " + section.Text + " were edited successfully", "OK");
var width = this.Width;
generateProfiles(width, stdprofiles, absentstds);
nbofstds.Text = stdprofiles.Count.ToString() + " Students";
submit.IsEnabled = true;
}
else
{
await DisplayAlert("Operation Failed", "Response Failed!", "Cancel");
submit.IsEnabled = true;
}
}
catch (System.Net.WebException exp)
{
submit.IsEnabled = true;
bool ans = await DisplayAlert("Connection Failed", "Please Check Your Internet Connection!", "Retry", "Cancel");
if (ans == true)
submit_Clicked(sender, e);
}
catch (Exception exp)
{
submit.IsEnabled = true;
bool ans = await DisplayAlert("Connection Failed", exp.Message, "Retry", "Cancel");
if (ans == true)
submit_Clicked(sender, e);
}
}
this is the code at the sql server side:
ALTER PROCEDURE [dbo].[getstddetailsupdate]
#stdDetailsTable as StdDetailsTypeTable readonly
AS
Begin
update [dbo].[students]
set [section]= X.section, [profilePic]=X.profilePic
FROM [dbo].[students],#stdDetailsTable X where [dbo].[students].id=X.id and [dbo].[students].YearId=X.YearId
end
this is how they appear:
when they tap on the circular frame, they can choose a pic and when they click submit, the pics chosen for each student will be uploaded. the problem is that when many pics are chosen, no pictures are uploaded. but if only one is chosen, it will be uploaded normally. why is that? i can't figure out why this is happening. thanks in advance
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);
I want to change the tab icon and page after selected, I have 2 pages but I want to change with 1 tab and change the icon and page after selected, how can I do that?
public MainPage()
{
InitializeComponent();
var login = new NavigationPage(new login())
{
Title = "login",
Icon = "login.png"
};
var register = new NavigationPage(new register())
{
Title = "register",
Icon = "register.png"
};
if(CurrentPage is register)
{
Children.Add(login);
}
else
{
Children.Add(register);
}
this.CurrentPageChanged += (object sender, EventArgs e) =>
{
var i = this.Children.IndexOf(this.CurrentPage);
if (i == 0)
{
login.Title = "login";
login.Icon = "login.png";
}
else
{
register.Title = "register";
register.Icon = "register.png";
}
};
You can create two layout and use button to switch between the two layouts:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
Content = firstlayout();
}
public StackLayout firstlayout() {
StackLayout stacklayout = new StackLayout
{
Margin = new Thickness(20),
Children =
{
new Label { Text = "Primary colors" },
new BoxView { Color = Color.Red },
new BoxView { Color = Color.Yellow },
new BoxView { Color = Color.Blue },
}
};
Button button = new Button
{
Text = "Click to change content2",
VerticalOptions = LayoutOptions.EndAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
HeightRequest = 60,
BackgroundColor = Color.Green,
TextColor = Color.White
};
button.Clicked += (sender, args) => this.Content = secondlayout();
stacklayout.Children.Add(button);
return stacklayout;
}
public StackLayout secondlayout()
{
StackLayout stacklayout = new StackLayout
{
Margin = new Thickness(20),
Children =
{
new Label { Text = "Secondary colors" },
new BoxView { Color = Color.Green },
new BoxView { Color = Color.Orange },
new BoxView { Color = Color.Purple }
}
};
Button button = new Button
{
Text = "Click to change content1",
VerticalOptions = LayoutOptions.EndAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
HeightRequest = 60,
BackgroundColor= Color.Green,
TextColor = Color.White
};
button.Clicked += (sender, args) => this.Content = firstlayout();
stacklayout.Children.Add(button);
return stacklayout;
}
}
Result:
Or you can change Application.Current.MainPage to different pages:
private void Button_Clicked(object sender, EventArgs e)
{
Application.Current.MainPage = new MainPage();
//Or
Application.Current.MainPage = new LoginPage();
}
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(""));
}
}
}
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;
}
}