I'm developing a Xamarin Forms application in the netstandard2.0 framework to deploy both on Android and iOS. The purpose of the program is to create a custom camera to capture and handle high resolution pictures from the back camera that I will send later on to a computer to be analysed.
I have managed to take photos at full resolution on iOS, but I struggle for Android on newer phones. On the first phone (Samsung SM-A510F, Android 7.0 API 24) I was able to take 12 MP pictures, which is the same as the native Camera app, but on the second phone (Samsung SM-A415F, Android 10.0 API 29) which has 3 back cameras (5MP, 8MP, 48MP) the resolution is limited to 3.5MP(2560*1440).
I have created a custom CameraPreview that I bind to my CameraPreviewRenderer and allows me to have one View linked to either native renderers.
CameraPreview in the shared code
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace XamPOC.CustomRenderer
{
public enum CameraOptions
{
Rear,
Front
}
public class CameraPreview : View
{
public static readonly BindableProperty CameraProperty = BindableProperty.Create(propertyName: "Camera", returnType: typeof(CameraOptions), declaringType: typeof(CameraPreview), defaultValue: CameraOptions.Rear);
public CameraOptions Camera
{
get
{
return (CameraOptions)GetValue(CameraProperty);
}
set
{
SetValue(CameraProperty, value);
}
}
public void CapturePhoto()
{
MessagingCenter.Send(this, "CapturePhoto");
}
}
}
CameraPreviewRenderer for Android
using Android.Content;
using Android.Hardware.Camera2;
using Android.Util;
using AndroidX.Camera.Core;
using AndroidX.Camera.Lifecycle;
using AndroidX.Core.Content;
using Java.Util.Concurrent;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using XamPOC;
using XamPOC.Droid;
using XamPOC.CustomRenderer;
using System;
using Android.App;
using Android.Graphics;
using Android.Hardware.Camera2.Params;
using AndroidX.Camera.Camera2.InterOp;
using AndroidX.Lifecycle;
using Java.Lang;
using XamPOC.Services;
using XamPOC.CustomRenderer.Droid;
using AndroidX.Camera.Core.Impl;
using AndroidX.Camera.Camera2.Internal.Compat;
using Android.Net.Wifi.Aware;
[assembly: ExportRenderer(typeof(CameraPreview), typeof(CameraPreviewRenderer))]
namespace XamPOC.Droid
{
public class CameraPreviewRenderer : ViewRenderer<CameraPreview, UICameraPreview>
{
private UICameraPreview _uiCameraPreview;
private IExecutorService _cameraExecutor;
private ImageCapture _imageCapture;
private CameraManager _cameraManager;
public CameraPreviewRenderer(Context context) : base(context)
{
_cameraManager = (CameraManager) Context.GetSystemService(Context.CameraService);
_cameraManager.GetCameraIdList();
}
protected override void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
MessagingCenter.Unsubscribe<CameraPreview>(this, "CapturePhoto");
}
if (e.NewElement != null)
{
if (Control == null)
{
_uiCameraPreview = new UICameraPreview(Context);
SetNativeControl(_uiCameraPreview);
_cameraExecutor = Executors.NewSingleThreadExecutor();
StartCamera();
}
MessagingCenter.Subscribe<CameraPreview>(this, "CapturePhoto", async (sender) => TakePhoto());
}
}
private void StartCamera()
{
var cameraProviderFuture = ProcessCameraProvider.GetInstance(Context);
cameraProviderFuture.AddListener(new Runnable(() =>
{
var cameraProvider = (ProcessCameraProvider)cameraProviderFuture.Get();
var preview = new Preview.Builder().Build();
preview.SetSurfaceProvider(_uiCameraPreview.previewView.SurfaceProvider);
var cameraSelector = new CameraSelector.Builder()
.RequireLensFacing(CameraSelector.LensFacingBack)
.Build();
_imageCapture = new ImageCapture.Builder()
.SetCaptureMode(ImageCapture.CaptureModeMaximizeQuality)
.Build();
try
{
cameraProvider.UnbindAll();
var list = cameraProvider.AvailableCameraInfos;
foreach (var cameraInfo in list)
{
var c = cameraInfo;
}
var camera = cameraProvider.BindToLifecycle(GetLifecycleOwner(), cameraSelector, preview, _imageCapture);
}
catch (System.Exception ex)
{
Log.Error("CameraPreviewRenderer", "Use case binding failed", ex);
}
}), ContextCompat.GetMainExecutor(Context));
}
private ILifecycleOwner GetLifecycleOwner()
{
if (Context is ILifecycleOwner lifecycleOwner)
{
return lifecycleOwner;
}
if (Context is Activity activity)
{
var lifecycleRegistry = new LifecycleRegistry((ILifecycleOwner)activity);
lifecycleRegistry.SetCurrentState(Lifecycle.State.Started);
return (ILifecycleOwner)lifecycleRegistry;
}
throw new InvalidOperationException("Context does not implement ILifecycleOwner");
}
public void TakePhoto()
{
if (_imageCapture == null)
return;
var fileService = DependencyService.Get<IFileService>();
var folder = fileService.GetPicturesFolder();
var photoFile = new Java.IO.File(System.IO.Path.Combine(folder, $"CCube{DateTime.Now:yyyyMMdd_HHmmss}.jpg"));
var outputOptions = new ImageCapture.OutputFileOptions.Builder(photoFile)
.Build();
_imageCapture.TakePicture(outputOptions, _cameraExecutor, new ImageCaptureCallback(photoFile));
}
private class ImageCaptureCallback : Java.Lang.Object, ImageCapture.IOnImageSavedCallback
{
private Java.IO.File _photoFile;
public ImageCaptureCallback(Java.IO.File photoFile)
{
_photoFile = photoFile;
}
public void OnError(ImageCaptureException exception)
{
Log.Error("CameraPreviewRenderer", "Photo capture failed: " + exception.Message);
}
public void OnImageSaved(ImageCapture.OutputFileResults outputFileResults)
{
Log.Debug("CameraPreviewRenderer", "Photo capture succeeded: " + _photoFile.AbsolutePath);
byte[] ImageBytes = System.IO.File.ReadAllBytes(_photoFile.AbsolutePath);
MessagingCenter.Send(this, "PhotoArray", ImageBytes);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_cameraExecutor?.Shutdown();
}
}
}
}
CameraPage that displays the camera
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="XamPOC.Views.CameraPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamPOC.CustomRenderer"
xmlns:vm="clr-namespace:XamPOC.ViewModels"
Title="CameraPage"
x:DataType="vm:CameraViewModel"
NavigationPage.HasNavigationBar="true">
<Grid Padding="0" BackgroundColor="Black">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition x:Name="ImageRow" Height="10*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:CameraPreview
x:Name="ImageView"
Grid.Row="1"
Grid.RowSpan="11"
Grid.Column="0"
Grid.ColumnSpan="12"
BindingContext="{Binding CameraPreview}" />
<ImageButton
Grid.Row="8"
Grid.RowSpan="3"
Grid.Column="4"
Grid.ColumnSpan="4"
Margin="10"
BackgroundColor="Transparent"
Clicked="Button_Clicked"
Source="Camera.png" />
<FlexLayout
Grid.ColumnSpan="12"
AlignContent="Center"
AlignItems="Center"
Direction="Row"
JustifyContent="SpaceEvenly">
<Label
HorizontalTextAlignment="Center"
Text="CLI"
TextColor="White"
WidthRequest="85" />
<Switch Toggled="Switch_Toggled" />
<Label
HorizontalTextAlignment="Center"
Text="DERM"
TextColor="White"
WidthRequest="85" />
</FlexLayout>
</Grid>
</ContentPage>
I also tried using the CameraXBasics repository, but I encounter the same issue (Resolution is not good enough), so I was wondering if that was the limit to a custom camera on Xamarin Forms.
Thank you for reading this far !