How can I implement a video recorder inside an application using xamarin forms?
You can use Xamarin.Plugin.Media. If it is not flexible enough for you, then you need to implement the video recorder using the native APIs. In which case you probably shouldn't be doing Xamarin.Forms at all, but if you insist that is the way.
Do you want to achieve the result like following GIF(Over 2M, SO cannot allow upload it )?
https://imgur.com/a/kpMl2ed
I achieve it in android by custom rendere. Here is code.
First of all, we should MediaRecorder to record the video.
public void startRecord(SurfaceView surfaceView)
{
Device.BeginInvokeOnMainThread(() => {
string path = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + "/test.mp4";
recorder = new MediaRecorder();
//If you want to rotate the video screen, you can use following code
//Camera camera = Camera.Open();
//Camera.Parameters parameters = camera.GetParameters();
//parameters.SetPreviewSize(640, 480);
//parameters.SetPictureSize(640, 480);
//camera.SetParameters(parameters);
//camera.SetDisplayOrientation(90);
//camera.Unlock();
//recorder.SetCamera(camera);
recorder.SetVideoSource(VideoSource.Camera);
recorder.SetAudioSource(AudioSource.Mic);
recorder.SetOutputFormat(OutputFormat.Default);
recorder.SetVideoEncoder(VideoEncoder.Default);
recorder.SetAudioEncoder(AudioEncoder.Default);
recorder.SetOutputFile(path);
recorder.SetPreviewDisplay(surfaceView.Holder.Surface);
recorder.Prepare();
recorder.Start();
});
}
If you want to achieve the record the video inside the application, you should use custom renderer to view. Use SurfaceView to see the camera view.First of all. please create a CameraPreview
public sealed class CameraPreview : ViewGroup, ISurfaceHolderCallback
{
public SurfaceView surfaceView;
ISurfaceHolder holder;
Camera.Size previewSize;
IList<Camera.Size> supportedPreviewSizes;
Camera camera;
IWindowManager windowManager;
MediaRecorder recorder;
public bool IsPreviewing { get; set; }
public Camera Preview {
get { return camera; }
set {
camera = value;
if (camera != null) {
supportedPreviewSizes = Preview.GetParameters().SupportedPreviewSizes;
RequestLayout();
}
}
}
public CameraPreview (Context context)
: base (context)
{
surfaceView = new SurfaceView (context);
AddView (surfaceView);
windowManager = Context.GetSystemService (Context.WindowService).JavaCast<IWindowManager>();
IsPreviewing = false;
holder = surfaceView.Holder;
holder.AddCallback (this);
MessagingCenter.Subscribe<string>("111", "Hi", (expense) =>
{
startRecord(surfaceView);
});
MessagingCenter.Subscribe<string>("1112", "Hi2", (expense) =>
{
stopRecord(surfaceView);
});
}
private void stopRecord(SurfaceView surfaceView)
{
recorder.Stop();
recorder.Release();
}
public void startRecord(SurfaceView surfaceView)
{
Device.BeginInvokeOnMainThread(() => {
string path = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + "/test.mp4";
recorder = new MediaRecorder();
//If you want to rotate the video screen, you can use following code
//Camera camera = Camera.Open();
//Camera.Parameters parameters = camera.GetParameters();
//parameters.SetPreviewSize(640, 480);
//parameters.SetPictureSize(640, 480);
//camera.SetParameters(parameters);
//camera.SetDisplayOrientation(90);
//camera.Unlock();
//recorder.SetCamera(camera);
recorder.SetVideoSource(VideoSource.Camera);
recorder.SetAudioSource(AudioSource.Mic);
recorder.SetOutputFormat(OutputFormat.Default);
recorder.SetVideoEncoder(VideoEncoder.Default);
recorder.SetAudioEncoder(AudioEncoder.Default);
recorder.SetOutputFile(path);
recorder.SetPreviewDisplay(surfaceView.Holder.Surface);
recorder.Prepare();
recorder.Start();
});
}
protected override void OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
{
int width = ResolveSize (SuggestedMinimumWidth, widthMeasureSpec);
int height = ResolveSize (SuggestedMinimumHeight, heightMeasureSpec);
SetMeasuredDimension (width, height);
if (supportedPreviewSizes != null) {
previewSize = GetOptimalPreviewSize (supportedPreviewSizes, width, height);
}
}
protected override void OnLayout (bool changed, int l, int t, int r, int b)
{
var msw = MeasureSpec.MakeMeasureSpec (r - l, MeasureSpecMode.Exactly);
var msh = MeasureSpec.MakeMeasureSpec (b - t, MeasureSpecMode.Exactly);
surfaceView.Measure (msw, msh);
surfaceView.Layout (0, 0, r - l, b - t);
}
public void SurfaceCreated (ISurfaceHolder holder)
{
try {
if (Preview != null) {
Preview.SetPreviewDisplay (holder);
}
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine (#" ERROR: ", ex.Message);
}
}
public void SurfaceDestroyed (ISurfaceHolder holder)
{
if (Preview != null) {
Preview.StopPreview ();
}
}
public void SurfaceChanged (ISurfaceHolder holder, Android.Graphics.Format format, int width, int height)
{
var parameters = Preview.GetParameters ();
parameters.SetPreviewSize (previewSize.Width, previewSize.Height);
RequestLayout ();
//If you want to rotate the video screen, you can use following code
//switch (windowManager.DefaultDisplay.Rotation) {
//case SurfaceOrientation.Rotation0:
// camera.SetDisplayOrientation (90);
// break;
//case SurfaceOrientation.Rotation90:
// camera.SetDisplayOrientation (0);
// break;
//case SurfaceOrientation.Rotation270:
// camera.SetDisplayOrientation (180);
// break;
//}
Preview.SetParameters (parameters);
Preview.StartPreview ();
IsPreviewing = true;
}
Camera.Size GetOptimalPreviewSize (IList<Camera.Size> sizes, int w, int h)
{
const double AspectTolerance = 0.1;
double targetRatio = (double)w / h;
if (sizes == null) {
return null;
}
Camera.Size optimalSize = null;
double minDiff = double.MaxValue;
int targetHeight = h;
foreach (Camera.Size size in sizes) {
double ratio = (double)size.Width / size.Height;
if (Math.Abs (ratio - targetRatio) > AspectTolerance)
continue;
if (Math.Abs (size.Height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.Abs (size.Height - targetHeight);
}
}
if (optimalSize == null) {
minDiff = double.MaxValue;
foreach (Camera.Size size in sizes) {
if (Math.Abs (size.Height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.Abs (size.Height - targetHeight);
}
}
}
return optimalSize;
}
}
Then, Here is custom renderer.
[assembly: ExportRenderer(typeof(CustomRenderer.CameraPreview), typeof(CameraPreviewRenderer))]
namespace CustomRenderer.Droid
{
public class CameraPreviewRenderer : ViewRenderer
{
CameraPreview cameraPreview;
public CameraPreviewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<CustomRenderer.CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
cameraPreview.Click -= OnCameraPreviewClicked;
}
if (e.NewElement != null)
{
if (Control == null)
{
cameraPreview = new CameraPreview(Context);
SetNativeControl(cameraPreview);
}
Control.Preview = Camera.Open((int)e.NewElement.Camera);
// Subscribe
cameraPreview.Click += OnCameraPreviewClicked;
}
}
void OnCameraPreviewClicked(object sender, EventArgs e)
{
if (cameraPreview.IsPreviewing)
{
cameraPreview.Preview.StopPreview();
cameraPreview.IsPreviewing = false;
}
else
{
cameraPreview.Preview.StartPreview();
cameraPreview.IsPreviewing = true;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Control.Preview.Release();
}
base.Dispose(disposing);
}
}
}
If you want to know how to achieve Camera Preview in the IOS or UWP, you can refer to this link.
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/view
Here is code in github.
https://github.com/851265601/FormsRecordVideoInside
Above way to achieve it, it a bit complexed. I suggest you to use the Xam.Plugin.Media to achieve it. Here is running GIF. it cannot see the video preview in the application, but you do not need to use custom renderer to achieve it just serveral lines code.
private async void PlayStopButtonText_Clicked(object sender, EventArgs e)
{
// throw new NotImplementedException();
var file = await CrossMedia.Current.TakeVideoAsync(new StoreVideoOptions
{
SaveToAlbum = true,Directory = "Sample"
});
//Get the public album path
if (file == null)
return;
await DisplayAlert("File Location", file.AlbumPath, "OK");
}
In the android, add the following permission.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Here is running GIF(Over 2M, SO cannot allow upload it ).
https://imgur.com/a/9WE4szZ
Related
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
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;
}
}
}
I am trying to get the above look on my tabbed bar in xamarin forms, i tried customizing the tabbed bar using renderer and still was not able to get the expected output
output i am getting
till now this is what i have tried
[assembly: ExportRenderer(typeof(BottomNavTabPage), typeof(BottomNavTabPageRenderer))]
namespace HealthMobile.Droid.Renderers
{
public class BottomNavTabPageRenderer : TabbedPageRenderer
{
private bool _isShiftModeSet;
public BottomNavTabPageRenderer(Context context)
: base(context)
{
}
protected override void OnVisibilityChanged(Android.Views.View changedView, [GeneratedEnum] ViewStates visibility)
{
base.OnVisibilityChanged(changedView, visibility);
var tabs = changedView.FindViewById<TabLayout>(Resource.Id.sliding_tabs);
if (tabs != null)
{
ViewGroup vg = (ViewGroup)tabs.GetChildAt(0);
int tabsCount = vg.ChildCount;
}
}
//protected override void DispatchDraw (global::Android.Graphics.Canvas canvas)
// {
// base.DispatchDraw (canvas);
// SetTabIcons();
// // var tabLayout = (TabLayout)GetChildAt(1);
// }
// private void SetTabIcons()
// {
// var element = this.Element;
// if (null == element)
// {
// return;
// }
// Activity activity = this.Context as Activity;
// if ((null != activity) && (null != activity.ActionBar) && (activity.ActionBar.TabCount > 0))
// {
// for (int i = 0; i < element.Children.Count; i += 1)
// {
// var tab = activity.ActionBar.GetTabAt(i);
// var page = element.Children[i];
// if ((null != tab) && (null != page) && (null != page.Icon)
// && (tab.CustomView == null))
// {
// var resourceId = activity.Resources.GetIdentifier(page.Icon.File.ToLowerInvariant(), "drawable", this.Context.PackageName);
// LinearLayout tabHeader
// = new LinearLayout(activity) { Orientation = Orientation.Vertical };
// ImageView tabImg = new ImageView(activity);
// TextView tabTitle = new TextView(activity);
// tabImg.SetImageResource(resourceId);
// tabTitle.Text = page.Title;
// tabTitle.SetTextColor(Android.Graphics.Color.White);
// tabHeader.AddView(tabTitle);
// tabHeader.AddView(tabImg);
// tab.SetCustomView(tabHeader);
// }
// }
// }
// }
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
{
base.OnElementChanged(e);
var childViews = GetAllChildViews(ViewGroup);
//tab.SetIcon(Resource.Drawable.icon);
var scale = Resources.DisplayMetrics.Density;
var paddingDp = 0;
var dpAsPixels = (int)(paddingDp * scale + 0.5f);
foreach (var childView in childViews)
{
if (childView is BottomNavigationItemView tab)
{
//tab.SetPadding(-50, -100, -50, -100);
}
else if (childView is TextView textView)
{
textView.SetTextColor(Android.Graphics.Color.Transparent);
}
}
}
protected override void SetTabIcon(TabLayout.Tab tab, FileImageSource icon)
{
base.SetTabIcon(tab, icon);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
try
{
if (!_isShiftModeSet)
{
var children = GetAllChildViews(ViewGroup);
if (children.SingleOrDefault(x => x is BottomNavigationView) is BottomNavigationView bottomNav)
{
bottomNav.SetShiftMode(false, false);
_isShiftModeSet = true;
}
}
}
catch (Exception e)
{
Console.WriteLine($"Error setting ShiftMode: {e}");
}
}
private List<View> GetAllChildViews(View view)
{
if (!(view is ViewGroup group))
{
return new List<View> {view };
}
var result = new List<View>();
for (int i = 0; i < group.ChildCount; i++)
{
var child = group.GetChildAt(i);
var childList = new List<View> {child};
childList.AddRange(GetAllChildViews(child));
result.AddRange(childList);
}
return result.Distinct().ToList();
}
}
}
i am trying to make this look like this somewhat
output expecting
also i tried setting up the icons but SetTabIcons method never get triggered
I want to customize my picker. I created a custom renderer for my picker but I dont know how the customization settings. How can I change the font style and size of the item? and How can I remove the two lines?
public class CustomPickerRenderer : PickerRenderer
{
public CustomPickerRenderer(Context context) : base(context)
{
AutoPackage = false;
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
Control.Background = null;
var layoutParams = new MarginLayoutParams(Control.LayoutParameters);
layoutParams.SetMargins(0, 0, 0, 0);
Control.LayoutParameters = layoutParams;
Control.SetPadding(0, 0, 0, 0);
SetPadding(0, 0, 0, 0);
}
}
}
If you want to set the fontSize of the text , you first need to customize a subclass extends from NumberPicker and overwrite the method AddView.
public class TextColorNumberPicker: NumberPicker
{
public TextColorNumberPicker(Context context) : base(context)
{
}
public override void AddView(View child, int index, ViewGroup.LayoutParams #params)
{
base.AddView(child, index, #params);
UpdateView(child);
}
public void UpdateView(View view)
{
if ( view is EditText ) {
//set the font of text
((EditText)view).TextSize = 8;
}
}
}
If you want to remove the lines,you should rewrite the NumberPicker
in Android Custom Renderer
public class MyAndroidPicker:PickerRenderer
{
IElementController ElementController => Element as IElementController;
public MyAndroidPicker()
{
}
private AlertDialog _dialog;
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
Control.Click += Control_Click;
}
protected override void Dispose(bool disposing)
{
Control.Click -= Control_Click;
base.Dispose(disposing);
}
private void SetPickerDividerColor(TextColorNumberPicker picker)
{
Field[] fields = picker.Class.GetDeclaredFields();
foreach (Field pf in fields)
{
if(pf.Name.Equals("mSelectionDivider"))
{
pf.Accessible = true;
// set the color as transparent
pf.Set(picker, new ColorDrawable(this.Resources.GetColor(Android.Resource.Color.Transparent)));
}
}
}
private void Control_Click(object sender, EventArgs e)
{
Picker model = Element;
var picker = new TextColorNumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Yellow);
picker.SetDisplayedValues(model.Items.ToArray());
//call the method after you setting DisplayedValues
SetPickerDividerColor(picker);
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
}
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
layout.AddView(picker);
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, true);
var builder = new AlertDialog.Builder(Context);
builder.SetView(layout);
builder.SetTitle(model.Title ?? "");
builder.SetNegativeButton("Cancel ", (s, a) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
_dialog = null;
});
builder.SetPositiveButton("Ok ", (s, a) =>
{
ElementController.SetValueFromRenderer(Picker.SelectedIndexProperty, picker.Value);
// It is possible for the Content of the Page to be changed on SelectedIndexChanged.
// In this case, the Element & Control will no longer exist.
if (Element != null)
{
if (model.Items.Count > 0 && Element.SelectedIndex >= 0)
Control.Text = model.Items[Element.SelectedIndex];
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is also possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
}
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
I also used this CustomRenderer which was posted before only instead of overriding it you can change the properties like this.
private void Control_Click(object sender, EventArgs e)
{
Picker model = Element;
var picker = new MyNumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
// set style here
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Transparent);
picker.SetDisplayedValues(model.Items.ToArray());
//call the method after you setting DisplayedValues
SetPickerDividerColor(picker);
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
// change Text Size and Divider
picker.TextSize = 30;
picker.SelectionDividerHeight = 1;
}
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.