Is there any way to make a popup with a few questions + a few answers to choose from after pressing the added pin on the map?
How to do that? What I mean most is how to combine the popup with the pin (in xamarin.forms)
I apologize for my weak language
My pin code for adding pins:
private async void OnButton(object sender, EventArgs e)
{
Pin pin = new Pin
{
Label = "Nazwa",
Address = "adres",
Type = PinType.SavedPin,
Position = new Position(Convert.ToDouble(szerokosc.Text), Convert.ToDouble(dlugosc.Text))
};
positions.Add(new Position(Convert.ToDouble(szerokosc.Text), Convert.ToDouble(dlugosc.Text)));
Polyline polyline = new Polyline
{
StrokeColor = Color.Blue,
StrokeWidth = 6
};
foreach (Position pos in positions)
{
polyline.Geopath.Add(pos);
}
maps.Pins.Add(pin);
maps.MapElements.Add(polyline);
var json = JsonConvert.SerializeObject(new { X = pin.Position.Latitude, Y = pin.Position.Longitude });
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpClient client = new HttpClient();
var result = await client.PostAsync("URL", content);
if (result.StatusCode == HttpStatusCode.Created)
{
await DisplayAlert("Komunikat", "Dodanie puntku przebiegło pomyślnie", "Anuluj");
}
}
You could use CustomRenderer to define your Map,and create a custom info window base on your needs.For example,use a listview to display your questions and answers.
You could refer to custom map pin and custom info window
Update (i could only give you some snippets,you should fill your content and layout base on your needs):
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace CustomRenderer.Droid
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter
{
List<CustomPin> customPins;
public CustomMapRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
NativeMap.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
customPins = formsMap.CustomPins;
}
}
protected override void OnMapReady(GoogleMap map)
{
base.OnMapReady(map);
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
}
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.pin));
return marker;
}
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (!string.IsNullOrWhiteSpace(customPin.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);
}
}
// you could custom your view with a listview here,fill the answers and question
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");
}
//inflate your custom layout
if (customPin.Name.Equals("Xamarin"))
{
view = inflater.Inflate(Resource.Layout.XamarinMapInfoWindow, null);
}
else
{
view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
}
var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
var infoSubtitle = view.FindViewById<TextView>(Resource.Id.InfoWindowSubtitle);
if (infoTitle != null)
{
infoTitle.Text = marker.Title;
}
if (infoSubtitle != null)
{
infoSubtitle.Text = marker.Snippet;
}
return view;
}
return null;
}
public 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.Position == position)
{
return pin;
}
}
return null;
}
}
}
Related
This post is continuation of this topic
I need find equivalent method Xamarin.Forms.Maps.iOS.MapRenderer.GetViewForAnnotation for CustomMapRenderer for Android.
As for this documentation the GetViewForAnnotation method for iOS is called when the location of the annotation becomes visible on the map, and is used to customize the annotation prior to display.
I need to find equivalent because I want to show pins on my map with already expanded window message(without click on them).
You could show the InfoWindow directly after you get the marker from your custom pin.
Add the code below when you override the CreateMarker method.
NativeMap.AddMarker(marker).ShowInfoWindow();
The whold custom renderer of Android:
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter
{
List<CustomPin> customPins;
public CustomMapRenderer(Context context) : base(context)
{
}
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;
customPins = formsMap.CustomPins;
}
}
protected override void OnMapReady(GoogleMap map)
{
base.OnMapReady(map);
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
}
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource(Resource.Drawable.pin));
NativeMap.AddMarker(marker).ShowInfoWindow();
return marker;
}
private 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);
}
}
CustomPin GetCustomPin(Marker annotation)
{
var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
foreach (var pin in customPins)
{
if (pin.Position == position)
{
return pin;
}
}
return null;
}
public 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.Name.Equals("Xamarin"))
{
view = inflater.Inflate(Resource.Layout.XamarinMapInfoWindow, null);
}
else
{
view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
}
var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
var infoSubtitle = view.FindViewById<TextView>(Resource.Id.InfoWindowSubtitle);
if (infoTitle != null)
{
infoTitle.Text = marker.Title;
}
if (infoSubtitle != null)
{
infoSubtitle.Text = marker.Snippet;
}
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
}
For more details, you could refer to the MS docs.
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/map-pin
In my xamarin.forms app, I created a custom camera by using Camera view and custom renders. Everything works fine. In android after the photo capture I can check whether the taken photo contains a face or not.It is done by using Camera.IFaceDetectionListener. My question is how can I achieve this in iOS? I know there is vision API. But I don't want the live face tracking. I just simply want to check whether the taken photo contains face. Any help is appreciated.
My iOS CameraPreview
public class UICameraPreview : UIView, IAVCaptureMetadataOutputObjectsDelegate
{
AVCaptureVideoPreviewLayer previewLayer;
public AVCaptureDevice[] videoDevices;
CameraOptions cameraOptions;
public AVCaptureStillImageOutput stillImageOutput;
public AVCaptureDeviceInput captureDeviceInput;
public AVCaptureDevice device;
public event EventHandler<EventArgs> Tapped;
public AVCaptureSession CaptureSession { get; set; }
public bool IsPreviewing { get; set; }
public AVCaptureStillImageOutput CaptureOutput { get; set; }
public UICameraPreview(CameraOptions options)
{
cameraOptions = options;
IsPreviewing = false;
Initialize();
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (previewLayer != null)
previewLayer.Frame = Bounds;
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
OnTapped();
}
protected virtual void OnTapped()
{
var eventHandler = Tapped;
if (eventHandler != null)
{
eventHandler(this, new EventArgs());
}
}
void Initialize()
{
CaptureSession = new AVCaptureSession();
previewLayer = new AVCaptureVideoPreviewLayer(CaptureSession)
{
Frame = Bounds,
VideoGravity = AVLayerVideoGravity.ResizeAspectFill
};
videoDevices = AVCaptureDevice.DevicesWithMediaType(AVMediaType.Video);
var cameraPosition = (cameraOptions == CameraOptions.Front) ? AVCaptureDevicePosition.Front : AVCaptureDevicePosition.Back;
device = videoDevices.FirstOrDefault(d => d.Position == cameraPosition);
if (device == null)
{
return;
}
NSError error;
captureDeviceInput = new AVCaptureDeviceInput(device, out error);
CaptureSession.AddInput(captureDeviceInput);
var dictionary = new NSMutableDictionary();
dictionary[AVVideo.CodecKey] = new NSNumber((int)AVVideoCodec.JPEG);
stillImageOutput = new AVCaptureStillImageOutput()
{
OutputSettings = new NSDictionary()
};
CaptureSession.AddOutput(stillImageOutput);
Layer.AddSublayer(previewLayer);
CaptureSession.StartRunning();
IsPreviewing = true;
}
// Photo Capturing
public async Task CapturePhoto()
{
try
{
var videoConnection = stillImageOutput.ConnectionFromMediaType(AVMediaType.Video);
var sampleBuffer = await stillImageOutput.CaptureStillImageTaskAsync(videoConnection);
var jpegData = AVCaptureStillImageOutput.JpegStillToNSData(sampleBuffer);
var photo = new UIImage(jpegData);
var rotatedPhoto = RotateImage(photo, 180f);
var img = rotatedPhoto;
CALayer layer = new CALayer
{
ContentsScale = 1.0f,
Frame = Bounds,
Contents = rotatedPhoto.CGImage //Contents = photo.CGImage,
};
CaptureSession.StopRunning();
photo.SaveToPhotosAlbum((image, error) =>
{
if (!string.IsNullOrEmpty(error?.LocalizedDescription))
{
Console.Error.WriteLine($"\t\t\tError: {error.LocalizedDescription}");
}
});
}
catch (Exception ex)
{
}
//MainPage.UpdateSource(UIImageFromLayer(layer).AsJPEG().AsStream());
//MainPage.UpdateImage(UIImageFromLayer(layer).AsJPEG().AsStream());
}
}
My CameraPreviewRenderer
public class CameraPreviewRenderer : ViewRenderer<CameraPreview, UICameraPreview>, IAVCaptureMetadataOutputObjectsDelegate
{
UICameraPreview uiCameraPreview;
AVCaptureSession captureSession;
AVCaptureDeviceInput captureDeviceInput;
AVCaptureStillImageOutput stillImageOutput;
protected override void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
uiCameraPreview.Tapped -= OnCameraPreviewTapped;
}
if (e.NewElement != null)
{
if (Control == null)
{
uiCameraPreview = new UICameraPreview(e.NewElement.Camera);
SetNativeControl(uiCameraPreview);
MessagingCenter.Subscribe<Camera_Popup>(this, "CaptureClick", async (sender) =>
{
try
{
// Using messeging center to take photo when clicking button from shared code
var data = new AVCaptureMetadataOutputObjectsDelegate();
await uiCameraPreview.CapturePhoto();
}
catch (Exception ex)
{
return;
}
});
}
MessagingCenter.Subscribe<Camera_Popup>(this, "RetryClick", (sender) =>
{
Device.BeginInvokeOnMainThread(() =>
{
uiCameraPreview.CaptureSession.StartRunning();
uiCameraPreview.IsPreviewing = true;
});
});
MessagingCenter.Subscribe<Camera_Popup>(this, "FlipClick", (sender) =>
{
try
{
var devicePosition = uiCameraPreview.captureDeviceInput.Device.Position;
if (devicePosition == AVCaptureDevicePosition.Front)
{
devicePosition = AVCaptureDevicePosition.Back;
}
else
{
devicePosition = AVCaptureDevicePosition.Front;
}
uiCameraPreview.device = uiCameraPreview.videoDevices.FirstOrDefault(d => d.Position == devicePosition);
uiCameraPreview.CaptureSession.BeginConfiguration();
uiCameraPreview.CaptureSession.RemoveInput(uiCameraPreview.captureDeviceInput);
uiCameraPreview.captureDeviceInput = AVCaptureDeviceInput.FromDevice(uiCameraPreview.device);
uiCameraPreview.CaptureSession.AddInput(uiCameraPreview.captureDeviceInput);
uiCameraPreview.CaptureSession.CommitConfiguration();
}
catch (Exception ex)
{
var abc = ex.InnerException.Message;
}
});
uiCameraPreview.Tapped += OnCameraPreviewTapped;
}
}
void OnCameraPreviewTapped(object sender, EventArgs e)
{
if (uiCameraPreview.IsPreviewing)
{
uiCameraPreview.CaptureSession.StopRunning();
uiCameraPreview.IsPreviewing = false;
}
else
{
uiCameraPreview.CaptureSession.StartRunning();
uiCameraPreview.IsPreviewing = true;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Control.CaptureSession.Dispose();
Control.Dispose();
}
base.Dispose(disposing);
}
}
The CoreImage framework has CIDetector that provides image detectors for faces, QR codes, text, .... in which you pass in an image and you get a specific "feature set" back.
Example from Xamarin docs:
var imageFile = "photoFace2.jpg";
var image = new UIImage(imageFile);
var context = new CIContext ();
var detector = CIDetector.CreateFaceDetector (context, true);
var ciImage = CIImage.FromCGImage (image.CGImage);
var features = detector.GetFeatures (ciImage);
Console.WriteLine ("Found " + features.Length + " faces");
re: https://learn.microsoft.com/en-us/dotnet/api/CoreImage.CIDetector?view=xamarin-ios-sdk-12
im trying to draw a path on map which user walked. It works fine on android but ios, just draw at start little line. After "OnElementPropertyChanged" triggered not draw anything.
RouteCoordinates are created on CustomMap based Map class as BindableProperties. I think my property changed method can not reach right display layer or thread.
My custom renderer for IOS:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace PawsApp.iOS.Renderers
{
public class CustomMapRenderer : MapRenderer
{
MKPolylineRenderer polylineRenderer;
CustomMap formsMap;
MKMapView nativeMap;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
if (nativeMap != null)
{
nativeMap.RemoveOverlays(nativeMap.Overlays);
nativeMap.OverlayRenderer = null;
polylineRenderer = null;
}
}
if (e.NewElement != null)
{
formsMap = (CustomMap)e.NewElement;
nativeMap = Control as MKMapView;
UpdatePolyLine();
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
//if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
if ((e.PropertyName == "RouteCoordinates" || e.PropertyName == "VisibleRegion"))
{
formsMap = (CustomMap)sender;
nativeMap = Control as MKMapView;
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
//var nativeMap = Control as MKMapView;
nativeMap.OverlayRenderer = GetOverlayRenderer;
CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[formsMap.RouteCoordinates.Count];
int index = 0;
foreach (var position in formsMap.RouteCoordinates)
{
coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
index++;
}
var routeOverlay = MKPolyline.FromCoordinates(coords);
nativeMap.AddOverlay(routeOverlay);
}
//[Foundation.Export("mapView:rendererForOverlay:")]
MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlayWrapper)
{
if (polylineRenderer == null)
{
var overlay = Runtime.GetNSObject(overlayWrapper.Handle) as IMKOverlay;
//var overlay = Runtime.GetNSObject(overlayWrapper.Handle) as MKPolyline;
polylineRenderer = new MKPolylineRenderer(overlay as MKPolyline)
{
FillColor = UIColor.Yellow,
StrokeColor = UIColor.Red,
LineWidth = 3,
Alpha = 0.4f
};
}
return polylineRenderer;
}
}
}
After trying it on my side, I find if you want to refresh the polyLine in the Map in iOS, you have to:
remove the old polyline;
use a new nativeMap.OverlayRenderer everytime.
I changed the customer renderer code like this:
public class CustomMapRenderer : MapRenderer
{
MKPolylineRenderer polylineRenderer;
CustomMap formsMap;
MKMapView nativeMap;
MKPolyline polyline;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
if (nativeMap != null)
{
nativeMap.RemoveOverlays(nativeMap.Overlays);
nativeMap.OverlayRenderer = null;
polylineRenderer = null;
}
}
if (e.NewElement != null)
{
formsMap = (CustomMap)e.NewElement;
nativeMap = Control as MKMapView;
UpdatePolyLine();
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Element == null || this.Control == null)
return;
//if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
if ((e.PropertyName == "RouteCoordinates" || e.PropertyName == "VisibleRegion"))
{
formsMap = (CustomMap)sender;
nativeMap = Control as MKMapView;
UpdatePolyLine();
}
}
private void UpdatePolyLine()
{
//var nativeMap = Control as MKMapView;
if (polyline != null)
{
nativeMap.RemoveOverlay(polyline);
polyline.Dispose();
}
nativeMap.OverlayRenderer = GetOverlayRenderer;
CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[formsMap.RouteCoordinates.Count];
int index = 0;
foreach (var position in formsMap.RouteCoordinates)
{
coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
index++;
}
var routeOverlay = MKPolyline.FromCoordinates(coords);
nativeMap.AddOverlay(routeOverlay);
polyline = routeOverlay;
}
//[Foundation.Export("mapView:rendererForOverlay:")]
MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlayWrapper)
{
if (polylineRenderer != null)
{
polylineRenderer = null;
}
var overlay = Runtime.GetNSObject(overlayWrapper.Handle) as IMKOverlay;
//var overlay = Runtime.GetNSObject(overlayWrapper.Handle) as MKPolyline;
polylineRenderer = new MKPolylineRenderer(overlay as MKPolyline)
{
FillColor = UIColor.Yellow,
StrokeColor = UIColor.Red,
LineWidth = 3,
Alpha = 0.4f
};
return polylineRenderer;
}
}
And in the xamarin.forms porject, reset the customMap.RouteCoordinates every time:
Device.StartTimer(TimeSpan.FromSeconds(3), () =>
{
Device.BeginInvokeOnMainThread(() =>
{
a -= 0.000321;
b += 0.000222;
customMap.RouteCoordinates = new List<Position>
{
new Position (37.797534, -122.401827),
new Position(37.797510, -122.402060),
new Position(37.790269, -122.400589),
new Position(37.790265, -122.400474),
new Position(37.790228, -122.400391),
new Position(37.790126, -122.400360),
new Position(37.789250, -122.401451),
new Position(a, b)
};
});
return true;
});
I also upload my demo here and you can check it. Let me know if it works.
Hi everyone, i had to create custom info-window in xamarin forms as shown in the screenshot above. I have created a custom renderer for that but the problem i am having right now is that buttons are not getting clicked. Xamarin is taking entire info-window as a clickable view. Please guide me what i am doing wrong or is it possible to achieve button clicks in Xamarin forms. Thanks in advance.
Here is the code for custom renderer:
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.Android;
using SalesApp.Droid.CustomRenderer;
using Android.Widget;
using SalesApp.CustomControls;
using SalesApp.Droid.Listeners;
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace SalesApp.Droid.CustomRenderer
{
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);
}
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
map.InfoWindowClick += OnInfoWindowClick;
map.SetInfoWindowAdapter(this);
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !isDrawn)
{
map.Clear();
if (customPins != null)
{
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((int)typeof(Resource.Drawable).GetField(pin.Image).GetValue(null)));
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);
//}
Android.App.Application.Context.StartActivity(new Intent(Android.App.Application.Context, typeof(DialogActivity)));
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
InvokeOnMapReadyBaseClassHack(googleMap);
map = googleMap;
map.SetInfoWindowAdapter(this);
map.InfoWindowClick += OnInfoWindowClick;
}
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 address = view.FindViewById<TextView>(Resource.Id.Address);
var contactPerson = view.FindViewById<TextView>(Resource.Id.ContactPerson);
var phone = view.FindViewById<TextView>(Resource.Id.Phone);
var zip = view.FindViewById<TextView>(Resource.Id.Zip);
var email = view.FindViewById<TextView>(Resource.Id.Email);
var cvr = view.FindViewById<TextView>(Resource.Id.CVR);
var turnover = view.FindViewById<TextView>(Resource.Id.Turnover);
var noOfEmp = view.FindViewById<TextView>(Resource.Id.NoOfEmp);
var leadStatus = view.FindViewById<TextView>(Resource.Id.LeadStatus);
var category = view.FindViewById<TextView>(Resource.Id.Cat);
if (infoTitle != null)
{
infoTitle.Text = customPin.LeedsAndCustomersData.FullName;
}
if (address != null)
{
address.Text = customPin.LeedsAndCustomersData.Address + " " + customPin.LeedsAndCustomersData.CityName;
}
if (contactPerson != null)
{
contactPerson.Text = customPin.LeedsAndCustomersData.FirstName;
}
if (phone != null)
{
phone.Text = customPin.LeedsAndCustomersData.Phone;
}
if (zip != null)
{
zip.Text = customPin.LeedsAndCustomersData.ZipCode;
}
if (email != null)
{
email.Text = customPin.LeedsAndCustomersData.Email;
}
if (cvr != null)
{
cvr.Text = customPin.LeedsAndCustomersData.CVR;
}
if (turnover != null)
{
turnover.Text = customPin.LeedsAndCustomersData.Turnover;
}
if (noOfEmp != null)
{
noOfEmp.Text = customPin.LeedsAndCustomersData.NoOfEmployees.ToString();
}
if (leadStatus != null)
{
leadStatus.Text = customPin.LeedsAndCustomersData.LeadStatus;
}
if (category != null)
{
category.Text = customPin.LeedsAndCustomersData.BusinessType;
}
//add listeners
OnTouchPhoneListener callButtonListener = new OnTouchPhoneListener();
phone.SetOnTouchListener(callButtonListener);
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;
}
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 });
}
}
}
}
OnInfoWindowElemTouchListener:
using Android.OS;
using Android.Views;
using Android.Widget;
using System.Threading.Tasks;
using Android.Gms.Maps.Model;
using Android.Graphics.Drawables;
using Java.Lang;
namespace SalesApp.Droid.Listeners
{
public abstract class OnInfoWindowElemTouchListener : Java.Lang.Object, View.IOnTouchListener
{
private View view;
private Drawable bgDrawableNormal;
private Drawable bgDrawablePressed;
private Handler handler = new Handler();
private Marker marker;
private static bool endPressStatus = false;
private bool pressed = false;
//public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed)
//{
// this.view = this.view;
// this.bgDrawableNormal = this.bgDrawableNormal;
// this.bgDrawablePressed = this.bgDrawablePressed;
//}
public OnInfoWindowElemTouchListener(View view)
{
this.view = this.view;
}
public OnInfoWindowElemTouchListener(Button button)
{
}
public OnInfoWindowElemTouchListener()
{
}
public void setMarker(Marker marker)
{
this.marker = this.marker;
}
/*public bool OnTouch(View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down)
{
// do stuff
return true;
}
if (e.Action == MotionEventActions.Up)
{
// do other stuff
return true;
}
return false;
}*/
public bool OnTouch(View vv, MotionEvent e)
{
if (0 <= e.GetX() && e.GetX() <= vv.Width && 0 <= e.GetY() && e.GetY() <= vv.Height)
{
switch (e.ActionMasked)
{
case MotionEventActions.Down:
startPress();
break;
// We need to delay releasing of the view a little so it shows the
// pressed state on the screen
case MotionEventActions.Up:
//handler.PostDelayed(ConfirmClickRunnable, 150);
//Task.Factory.StartNew(() =>onClickConfirmed(view, marker));
Task.Factory.StartNew(() => onClickConfirmed());
Task.Delay(150);
break;
case MotionEventActions.Cancel:
endPress();
break;
default:
break;
}
}
else
{
// If the touch goes outside of the view's area
// (like when moving finger out of the pressed button)
// just release the press
endPress();
}
return false;
}
private void startPress()
{
if (!pressed)
{
pressed = true;
//handler.RemoveCallbacks(ConfirmClickRunnable);
if ((marker != null))
{
marker.ShowInfoWindow();
}
}
}
public bool endPress()
{
if (pressed)
{
this.pressed = false;
//handler.RemoveCallbacks(ConfirmClickRunnable);
view.SetBackgroundColor(Android.Graphics.Color.Green);
if ((marker != null))
{
marker.ShowInfoWindow();
}
endPressStatus = true;
return true;
}
else
{
endPressStatus = false;
return false;
}
}
//private Runnable confirmClickRunnable = new RunnableAnonymousInnerClassHelper(this);
private Runnable ConfirmClickRunnable = new Java.Lang.Runnable(() =>
{
if (endPressStatus)
{
//onClickConfirmed(view, marker);
}
});
/*private class RunnableAnonymousInnerClassHelper : Java.Lang.Object, Java.Lang.IRunnable
{
private readonly Context outerInstance;
public RunnableAnonymousInnerClassHelper(Context outerInstance)
{
this.outerInstance = outerInstance;
}
public void Run()
{
if (endPressStatus)
{
onClickConfirmed();
}
}
}*/
//public abstract void onClickConfirmed(View v, Marker marker);
public abstract void onClickConfirmed();
}
}
OnTouchPhoneListener:
using Android.Widget;
using System;
namespace SalesApp.Droid.Listeners
{
public class OnTouchPhoneListener : OnInfoWindowElemTouchListener
//public class OnTouchPhoneListener
{
Button button;
public OnTouchPhoneListener(Button button)
{
}
public OnTouchPhoneListener()
{
}
public override void onClickConfirmed() {
Console.WriteLine("call Button Clicked");
}
}
}
Why is the info window implemented in your custom renderer? This makes the class CustomMapRenderer do (at least) do two things, violate the SRP and makes the code way harder to understand.
Instead I'd go with the custom renderer just for the map. Then in Xamarin.Forms you can implement the overlay with Xamarin.Forms means (e.g. the map view in an absolute or relative layout and the overlay in the same layout but a reduced size). Please see the following pseudo-XAML
<ContentPage [...]>
<AbsoluteLayout>
<local:MapView x:Name="MapView" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1" />
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" />
</AbsoluteLayout>
</ContentPage>
Then you can introduce a viewmodel PinInfoViewModel which is exposed by MapView.SelectedPinInfo and can be set as MapOverlay.BindingContext
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" />
MapOverlay - in turn - binds to all the properties of PinInfoViewModel. Last thing we'll have to do is making the overlay invisible if no pin is selected. For this purpose we expose MapView.IsPinSelected and bind MapOverlay.IsVisible to it
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" IsVisible="{Binding Source=MapView, Path=IsPinSelected}" />
While up to this point we have not done anything to solve your issue, you can now implement the overlay way simpler, e.g. with a StackLayout. And you can bind the Buttons commands to do whatever you'd like to do.
I am facing the following issue.
I am developing a Cross Platform Project on Xamarin and I am trying to open a ContentPage from a MapInfoWindow.
The Map ContentPage is inside the Portable Project and inside the Droid Project I have a the following class that is where I am trying to open the ContenPage:
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);
}
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
map.InfoWindowClick += OnInfoWindowClick;
map.SetInfoWindowAdapter(this);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !isDrawn)
{
map.Clear();
if (customPins != null)
{
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");
}
//Here I want to open the content page
}
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;
}
}
So I believe creating an EventHandler in your CustomMap class might be the way to go. In your CustomMap class, add the following:
public event EventHandler InfoTapped;
public virtual void OnInfoTapped(EventArgs e)
{
EventHandler handler = InfoTapped;
if (handler != null)
{
handler(this, e);
}
}
Then in your Forms shared code subscribe to the InfoTapped event and push your new ContentPage:
customMap.InfoTapped += async (sender, e) => {
await Navigation.PushAsync(new ContentPage());
};
Now in your renderer, create a class level field to hold a reference to your CustomMap:
CustomMap formsMap;
And then set this field in your OnElementChanged method:
if (e.NewElement != null)
{
// Remove the var to set your CustomMap field created
// above so you can use it elsewhere in the class
formsMap = (CustomMap)e.NewElement;
...
}
Now you can raise the event you created in CustomMap by calling:
formsMap.OnInfoTapped(e);
E.G.:
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
//Here I want to open the content page
formsMap.OnInfoTapped(e);
}
And the code you added to the customMap.InfoTapped event handler will be called, in this case to push a new page.