You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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新增静态照片输出能力,绑定拍照触发逻辑:

  1. 让渲染器实现IAVCapturePhotoCaptureDelegate接口,处理拍照结果回调
  2. 初始化AVCapturePhotoOutput并加入捕获会话
  3. 命令触发时调用拍照接口,拿到字节流后回传共享层
// 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配置新增静态图片捕获能力:

  1. 初始化ImageReader配置为JPEG输出格式,注册图片可用监听
  2. 拍照命令触发时构建静态捕获请求,将ImageReader的Surface加入请求目标
  3. 回调中取出字节流回传共享层,拍完恢复预览流
// 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")
  • 如果需要FileResult类型对象,将字节流写入临时文件后,直接通过new FileResult(临时文件路径)构造即可,完全不需要依赖MediaPicker组件。

内容的提问来源于stack exchange,提问作者Ailex 62534

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:33:18