Xamarin双平台如何实现不调系统相机且不存相册的拍照按钮
Xamarin 跨平台自定义相机内嵌拍照实现(iOS/Android)
关于返回类型的说明
自定义相机方案拍摄得到的原始结果不是FileResult类型。FileResult是Xamarin.Essentials.MediaPicker组件封装后的专属返回类型,自行实现的原生捕获逻辑拿到的原始数据是JPEG格式字节流,可根据业务需求自行转换格式、选择存储路径,不会被强制写入系统公共存储。
实现前提
你已经完成自定义CameraView控件的跨平台抽象、双平台渲染器的相机预览流搭建,以下步骤仅扩展拍照触发能力,全程不需要跳转系统相机应用,拍摄文件默认存储在应用私有目录,不会自动同步到系统相册。
步骤1:扩展共享层自定义控件
在共享项目的CameraView类中新增拍照命令、拍摄完成回传事件,用于和页面按钮、业务逻辑绑定:
// 共享项目 CustomControls/CameraView.cs public class CameraView : View { // 保留原有预览相关的所有属性、逻辑(如摄像头切换、预览启停等) // ... // 拍照触发绑定命令 public static readonly BindableProperty TakePhotoCommandProperty = BindableProperty.Create( nameof(TakePhotoCommand), typeof(ICommand), typeof(CameraView), default(ICommand)); public ICommand TakePhotoCommand { get => (ICommand)GetValue(TakePhotoCommandProperty); set => SetValue(TakePhotoCommandProperty, value); } // 拍摄完成事件,回传JPEG格式图片字节数组 public event EventHandler<byte[]> PhotoCaptured; // 平台渲染器调用的回传方法 public void TriggerPhotoCaptured(byte[] photoBytes) { PhotoCaptured?.Invoke(this, photoBytes); } }
在承载相机的页面XAML中添加拍照按钮,直接绑定控件的拍照命令即可:
<Grid> <controls:CameraView x:Name="CustomCamera" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" PhotoCaptured="CustomCamera_PhotoCaptured"/> <Button Text="拍照" Command="{Binding Source={x:Reference CustomCamera}, Path=TakePhotoCommand}" VerticalOptions="End" HorizontalOptions="Center" Margin="0,0,0,40"/> </Grid>
页面后台的CustomCamera_PhotoCaptured方法中即可拿到拍摄的字节流,直接做后续业务处理(预览、本地存储、上传等)。
步骤2:iOS平台拍照逻辑实现
在iOS项目的CameraView渲染器中,基于已有的AVCaptureSession新增静态照片输出能力,绑定拍照触发逻辑:
- 让渲染器实现
IAVCapturePhotoCaptureDelegate接口,处理拍照结果回调 - 初始化
AVCapturePhotoOutput并加入捕获会话 - 命令触发时调用拍照接口,拿到字节流后回传共享层
// iOS项目 Renderers/CameraViewRenderer.cs public class CameraViewRenderer : ViewRenderer<CameraView, UIView>, IAVCapturePhotoCaptureDelegate { private AVCaptureSession _captureSession; private AVCapturePhotoOutput _photoOutput; // 保留原有预览层、会话初始化等所有逻辑 // ... protected override void OnElementChanged(ElementChangedEventArgs<CameraView> e) { base.OnElementChanged(e); if (e.NewElement != null) { // 原有预览初始化逻辑执行完成后,初始化拍照输出 if (_photoOutput == null) { _photoOutput = new AVCapturePhotoOutput(); if (_captureSession.CanAddOutput(_photoOutput)) { _captureSession.AddOutput(_photoOutput); } } // 绑定拍照触发逻辑 e.NewElement.TakePhotoCommand = new Command(() => { var settings = AVCapturePhotoSettings.Create(); settings.FlashMode = AVCaptureFlashMode.Auto; _photoOutput.CapturePhoto(settings, this); }); } } // 拍照完成系统回调 [Export("captureOutput:didFinishProcessingPhoto:error:")] public void DidFinishProcessingPhoto(AVCapturePhotoOutput output, AVCapturePhoto photo, NSError error) { if (error != null || photo == null) return; // 提取JPEG字节数据 using var data = photo.FileDataRepresentation; byte[] photoBytes = new byte[data.Length]; Marshal.Copy(data.Bytes, photoBytes, 0, (int)data.Length); // 回传共享层,禁止调用PHPhotoLibrary.PerformChanges API即可避免写入系统相册 InvokeOnMainThread(() => Element.TriggerPhotoCaptured(photoBytes)); } }
步骤3:Android平台拍照逻辑实现
在Android项目的CameraView渲染器中,基于已有的Camera2 API配置新增静态图片捕获能力:
- 初始化
ImageReader配置为JPEG输出格式,注册图片可用监听 - 拍照命令触发时构建静态捕获请求,将
ImageReader的Surface加入请求目标 - 回调中取出字节流回传共享层,拍完恢复预览流
// Android项目 Renderers/CameraViewRenderer.cs public class CameraViewRenderer : ViewRenderer<CameraView, FrameLayout>, CameraCaptureSession.ICaptureListener { private CameraDevice _cameraDevice; private CameraCaptureSession _captureSession; private ImageReader _imageReader; private Surface _previewSurface; // 保留原有预览尺寸计算、相机打开、预览会话初始化等逻辑 // ... protected override void OnElementChanged(ElementChangedEventArgs<CameraView> e) { base.OnElementChanged(e); if (e.NewElement != null) { // 原有预览初始化流程中,新增ImageReader初始化 if (_imageReader == null) { _imageReader = ImageReader.NewInstance(previewWidth, previewHeight, ImageFormatType.Jpeg, 2); _imageReader.SetOnImageAvailableListener(new PhotoAvailableListener(OnPhotoCaptured), null); } // 绑定拍照触发逻辑 e.NewElement.TakePhotoCommand = new Command(() => { try { var captureBuilder = _cameraDevice.CreateCaptureRequest(CameraTemplate.StillCapture); captureBuilder.AddTarget(_previewSurface); // 保留预览流不中断 captureBuilder.AddTarget(_imageReader.Surface); captureBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.ContinuousPicture); _captureSession.Capture(captureBuilder.Build(), this, null); } catch { /* 按需处理拍照异常 */ } }); } } private void OnPhotoCaptured(ImageReader reader) { using var image = reader.AcquireLatestImage(); if (image == null) return; // 提取JPEG字节数据 var buffer = image.GetPlanes()[0].Buffer; byte[] photoBytes = new byte[buffer.Remaining()]; buffer.Get(photoBytes); // 回传共享层,禁止调用MediaStore.Images.Media.InsertImage、禁止向公共DCIM目录写文件即可避免写入系统相册 (Context as Activity)?.RunOnUiThread(() => { Element.TriggerPhotoCaptured(photoBytes); // 拍完恢复预览流 SetRepeatingPreviewRequest(); }); } // 实现捕获会话监听接口的其他方法,保留原有逻辑即可 public void OnCaptureComplete(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result){} public void OnCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure){} public void OnCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult){} public void OnCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, long frameNumber){} public void OnCaptureSequenceAborted(CameraCaptureSession session, int sequenceId){} public void OnCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber){} } // 内部图片可用监听器 public class PhotoAvailableListener : Java.Lang.Object, ImageReader.IOnImageAvailableListener { private readonly Action<ImageReader> _callback; public PhotoAvailableListener(Action<ImageReader> callback) => _callback = callback; public void OnImageAvailable(ImageReader reader) => _callback?.Invoke(reader); }
后续处理说明
拿到回传的byte[]后,可按需处理:
- 直接转为
MemoryStream绑定到ImageSource做拍摄结果预览 - 存储到应用私有目录,不会被系统相册扫描到:
- iOS存储路径:
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "captures", $"{Guid.NewGuid()}.jpg") - Android存储路径:
Path.Combine(Application.Context.FilesDir.AbsolutePath, "captures", $"{Guid.NewGuid()}.jpg")
- iOS存储路径:
- 如果需要
FileResult类型对象,将字节流写入临时文件后,直接通过new FileResult(临时文件路径)构造即可,完全不需要依赖MediaPicker组件。
内容的提问来源于stack exchange,提问作者Ailex 62534
相关产品推荐
相关产品推荐

