简介:面向 Xamarin.Forms 开发者的相机调用示例,演示如何借助依赖服务在共享代码中定义相机接口,并在 iOS 与 Android 平台分别实现拍照取图的完整链路。项目涵盖启动相机、拍摄照片、返回图片等核心操作,同时处理了 Android 6.0 及以上的运行时权限、iOS 的相机权限声明,以及照片流转换为显示对象、按钮触发、页面布局等交互细节,适合需要为跨平台 App 快速集成相机功能的中级开发者参考。压缩包文件共 83 份,以 C# 源代码、XAML 页面、PNG 图片资源和 iOS 配置文件为主,另有解决方案与工程配置,整体仅 286KB,结构紧凑便于检索。已有 307 人学习下载,既可用来理解依赖服务模式,也可作为可复用的相机模块样例。对于想要掌握 Xamarin.Forms 原生功能调用或快速实现拍照功能的开发者,这是一份轻量而实用的参考资料。
1. Xamarin.Forms 调相机:一个示例项目解决跨平台拍照的哪些麻烦
在 Xamarin.Forms 里把相机照片弄进应用,最容易翻车的地方不在拍照那行代码,而在拍照前后的平台差异上。Android 侧要面对运行时权限、厂商定制相机(华为、小米等)的返回行为差异;iOS 侧只要 Info.plist 漏写使用描述,应用当场闪退。这个示例项目把「调起系统相机 → 等待拍摄 → 拿回图片」整条链路用 C# 收拢到一个共享代码层,你只需要一处调用,就能在 Android、iOS、UWP 三个端完成图片采集。它适合两类人:一类是刚接触 Xamarin.Forms、想尽快把拍照跑通的新手;另一类是被 Android 碎片化设备折磨过、想对照成熟实现排查问题的老开发。接下来我把示例里的选型逻辑、权限配置、拍照代码和踩坑记录逐个拆开,每一步都能照着复现。
2. 认识 Xamarin.Forms 相机调用的三条路径:选型理由与代码分工
2.1 三条访问相机的路径:MediaPicker、传统插件与自定义渲染器
动手写代码之前,先把方案选型说透。这个示例项目核心走的是 Xamarin.Essentials 的 MediaPicker 路线,也就是当前 Xamarin.Forms 应用里调用相机最主流的方式。它的本质是调起系统自带的相机应用,拍完照片后把临时文件路径返回给应用,你再自己去读取和展示。
除了 MediaPicker,还有两条路。第一条是 Xamarin.Media / MediaPlugin 这类第三方插件,发布于 2016 年前后,当年是标配,但停更多年以后在 Android 11 设备上会频繁出现文件泄露或回调不到的问题;第二条是自定义渲染器或者 CameraView,把相机预览直接嵌进页面,可以做扫码、人脸识别、美颜这种定制效果,但实现成本高出一个量级。
为什么示例选 MediaPicker 而不是另两条路?理由很务实。系统相机方案把拍照动作交给系统应用,你不需要维护相机会话、对焦、快门、闪光灯这一堆原生逻辑,权限也只需要相机和存储两项;如果直接上自定义渲染器,光 Android 一个 SurfaceView 的生命周期控制就够写上百行。示例项目的目标是让你快速跑通「拍照发送/上传」这个高频场景,而不是研究相机底层,所以 MediaPicker 是最合理的起点,等有定制需求了再换路不迟。
MediaPicker 的调用链也不复杂:你发起请求以后,系统相机被唤起,用户按下快门或取消,随后回到应用,应用拿到一个 FileResult 对象。FileResult 是 Xamarin.Essentials 里对临时文件的一个封装,里面包含文件路径、文件名和 MIME 类型,你可以用 OpenReadAsync() 把它读成 Stream,再去做图片压缩、上传或者写入本地目录。
这个封装本身解决了一个很隐蔽的问题:Android 上系统相机的返回结果可能是一个 content:// URI,而不是真实文件路径。如果你直接拿 URI 去 File.Open 读,绝大多数机型会抛 FileNotFoundException;MediaPicker 则帮你把 URI 对应的文件复制到了应用专属的 CacheDir 临时目录,再通过 FileResult 返回相对稳定的本地路径。这个细节就是示例代码里大量调用 FileResult 而不是 string 路径的原因。
2.2 示例项目代码结构:共享项目、Android 与 iOS 各管哪一块
看这个示例的工程结构,你会发现它不是单靠共享代码就搞定一切的。一个标准的 Xamarin.Forms 解决方案里有三个项目:共享的 CameraDemo 项目负责页面和拍照调用,CameraDemo.Android 负责 Android 权限与原生资源配置,CameraDemo.iOS 负责 iOS 的 Info.plist 和原生适配。
拿图库选择这个功能举例,共享代码里写的是 MediaPicker.PickPhotoAsync(),但到了 Android 上,实际是系统图库应用被唤起,选完图后原图被复制到了 /data/user/0/PACKAGE/cache/ 某个临时位置;到了 iOS 上,则是 UIImagePickerController 被显示,返回时文件名变成类似 IMG_1234.JPG 的形式。把你的业务逻辑放在共享层、把平台差异留给框架,这正是 MediaPicker 存在的意义。
但有一点你得清楚:MediaPicker 无法把所有的原生差异都抹平。iOS 的相册选择回调可能给你压缩过的 JPEG 版本,Android 则可能是原始分辨率;同一套代码在不同系统上返回的文件大小、旋转方向、临时文件生命周期都不同。示例项目的做法是在共享层把 FileResult 尽快复制到应用沙箱目录,再做一次统一的尺寸校验和缩略图生成,而不是直接使用临时文件路径。
从文件层面来看,这个示例的核心文件集中在几个地方:MainPage.xaml 放两个按钮和一张图片展示区域,MainPage.xaml.cs 里写拍照与选图的异步方法,Services/CameraService.cs 放权限判断和文件处理逻辑。这样的分层方式让「相机关联代码」和「页面展示代码」保持解耦,你要是拿这个示例改造成业务模块,直接替换 CameraService 的实现就能接上自己的后端上传逻辑。
3. 把相机示例跑起来:权限配置、拍照代码与图片参数调优
3.1 权限配置:Android 的 Manifest 与 iOS 的 Info.plist
权限配置是相机功能能否生效的第一步,也是示例项目中触发问题最多的地方。先看 Android 端的 AndroidManifest.xml,你需要在 manifest 节点内声明相机和存储权限,并且加上 uses-feature 表示相机不是强制硬件,这样应用才能在模拟器或无相机设备上正常安装。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.companyname.camerademo" android:versionCode="1" android:versionName="1.0"> <!-- 拍照必须的相机权限 --> <uses-permission android:name="android.permission.CAMERA" /> <!-- 读取外部存储,Android 10 及以下从相册选图需要 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 写入外部存储,Android 10 及以下保存图片需要 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 声明相机硬件可选,避免无相机模拟器安装失败 --> <uses-feature android:name="android.hardware.camera" android:required="false" /> </manifest>这里要留个心眼:Android 11(API 30)之后,分区存储机制发生了改变,WRITE_EXTERNAL_STORAGE 在部分机型上不再直接生效,如果项目 targetSdkVersion 设到了 30 或以上,你需要额外使用 requestLegacyExternalStorage 兼容旧路径。示例中默认 targetSdk 是 29,目的是让新手在低版本设备上也能跑通,实际新项目里建议按 Android 官方要求处理分区存储,别照抄旧配置。
iOS 端要改的是 Info.plist。iOS 对相机和相册的隐私保护极其严格,如果你在代码里调用了 MediaPicker.CapturePhotoAsync 但 Info.plist 里没有声明 NSCameraUsageDescription,应用在调用瞬间就会被系统强制终止,没有任何错误提示。很多新手在这个问题上反复翻车,以为是代码写错了。
<key>NSCameraUsageDescription</key> <string>需要使用相机拍摄照片,用于上传个人头像</string> <key>NSPhotoLibraryUsageDescription</key> <string>需要访问相册以选择一张图片上传</string>两个描述字符串会直接显示在系统权限弹窗里,用户看到的就是这段文字,所以别写「方便调试」这种话,会被审核人员打回。另一个容易漏的是 NSMicrophoneUsageDescription,如果你后面扩展成拍视频功能,不声明麦克风描述,iOS 一样会崩掉,示例因为只做拍照没有涉及,但你要改造成拍视频就得提前加上。
3.2 用 MediaPicker 拍照的 C# 代码与参数说明
权限配置完成后,核心代码就集中在共享项目里。示例的 CameraService.cs 里封装了一个拍照方法,以下是最简可用版本。
using System; using System.IO; using System.Threading.Tasks; using Xamarin.Essentials; public static class CameraService { /// 拍照并返回复制到应用沙箱的本地文件路径;用户取消或出错时返回 null public static async Task<string> CapturePhotoAsync() { try { // 调起系统相机界面,等待用户拍摄或点取消 FileResult result = await MediaPicker.CapturePhotoAsync(new MediaCaptureOptions { PhotoSize = PhotoSize.Medium, // 压缩长边到约 1920px,防止原始分辨率爆内存 CompressionQuality = 90 // JPEG 压缩质量,90 是体积和画质的平衡点 }); if (result == null) { return null; // 用户在系统相机里取消了拍摄 } // FileResult 指向一个临时文件,必须尽快复制到应用目录,否则随时可能被系统回收 string targetPath = Path.Combine(FileSystem.AppDataDirectory, $"capture_{DateTime.Now:yyyyMMdd_HHmmss}.jpg"); using (Stream sourceStream = await result.OpenReadAsync()) using (FileStream targetStream = File.Create(targetPath)) { await sourceStream.CopyToAsync(targetStream); } return targetPath; } catch (PermissionException ex) { // 权限被拒绝时走到这里,可在这里弹窗引导用户去系统设置打开权限 System.Diagnostics.Debug.WriteLine($"拍照权限异常: {ex.Message}"); return null; } } }这段代码有四个关键点。第一,MediaPicker.CapturePhotoAsync 必须在 UI 线程被调用,因为它需要弹出系统界面,如果你在后台线程直接调用,Android 上会报 Android.Views.WindowManager.BadTokenException;第二,PhotoSize.Medium 是性能和画质权衡的推荐值,如果你需要原图做业务,可以改成 PhotoSize.Full,但要做好大图内存管理的心理准备,后面避坑章节会详细说;第三,result.OpenReadAsync() 返回的是只读流,读取完成前这个临时文件不能删除;第四,方法里复制到 FileSystem.AppDataDirectory 这一步是整个示例的精髓,AppDataDirectory 是应用私有目录,系统不会自动清理,而 CacheDir 里的临时文件可能会在系统重启后被清空,导致你持有的路径失效。
拿到返回的路径后,页面代码只需要做绑定展示。在 MainPage.xaml 里放一个 Image 控件,后台用 ImageSource.FromFile 绑定刚才复制的本地路径,图片就能显示出来。
// 页面里调用相机服务的示例 private async void OnTakePhotoClicked(object sender, EventArgs e) { // 首次调用时会自动触发运行时权限弹窗 string photoPath = await CameraService.CapturePhotoAsync(); if (photoPath != null) { // 清空之前的图片缓存,避免 Image 控件一直展示旧照片 PhotoImage.Source = null; PhotoImage.Source = ImageSource.FromFile(photoPath); } else { await DisplayAlert("提示", "拍照已取消或权限未授予", "确定"); } }ImageSource.FromFile 在 Android 上要求传入绝对路径,所以 CameraService 返回的必须是 Path.Combine 之后包含完整目录的结果,不能只返回 result.FileName 这种短文件名。iOS 上相对宽容一些,但为了跨端一致,示例里统一用绝对路径。
3.3 从相册选图:PickPhotoAsync 与 FullResolution 参数边界
相册选图在业务里出现的频率比拍照更高,毕竟大多数用户更愿意从已有照片里挑一张发出去。示例里选图的代码和拍照几乎对称,只是把 CapturePhotoAsync 换成 PickPhotoAsync,并且不需要指定 PhotoSize,因为系统返回的就是一种可用的预览尺寸。
public static async Task<string> PickPhotoAsync() { try { // 调起系统相册选择界面,等待用户勾选 FileResult result = await MediaPicker.PickPhotoAsync(new MediaPickerOptions { Title = "从相册选择一张照片" }); if (result == null) { return null; } // 相册返回的可能是经系统转码后的 JPEG,不是原始文件的完整精度 string copyPath = Path.Combine(FileSystem.AppDataDirectory, $"picked_{DateTime.Now:yyyyMMdd_HHmmss}.jpg"); using (Stream sourceStream = await result.OpenReadAsync()) using (FileStream targetStream = File.Create(copyPath)) { await sourceStream.CopyToAsync(targetStream); } return copyPath; } catch (PermissionException ex) { System.Diagnostics.Debug.WriteLine($"相册权限异常: {ex.Message}"); return null; } }关于「原图」这个概念,你要有一个预期管理。Windows 用户可能觉得相册里选了哪张就应该拿到哪张,实际上 iOS 的 UIImagePickerController 在默认模式下会对照片做一次转码,可能丢失原始 RAW 数据或者压缩了分辨率。MediaPicker 的 PickPhotoAsync 在 iOS 上也是一个封装,它返回的文件大小通常比相册里那张原图小。
如果你真的需要 FullResolution 级别的原图,用 MediaPicker 本身做不到,得在 iOS 平台工程里写 PHImageManager 的代码,并设置 PHImageRequestOptions 的 DeliveryMode 为 HighQualityFormat,然后通过自定 Renderer 或 Service 把结果传回共享层。示例项目没有做这一步,因为它默认业务场景是「拍完压缩上传」,不是「拍摄无损原图」。你要做地图标记、图像识别这类需要精度的功能,再自己补原生层。
这里还有个实际应用中的体验点:从相册选完图后,Image 控件刷新时建议先做采样缩放,而不是直接把图塞进 Source。做法是读取图片的宽高,计算一个合适的 InSampleSize,用 BitmapFactory.Options 只加载缩略图进内存。示例里因为在 CapturePhotoAsync 阶段已经用 PhotoSize.Medium 压过了,选图路径返回的也不会是 4000 万像素那种超大尺寸,所以展示层可以不处理。但你要预留后路,别让示例项目里省掉的这个环节变成你业务里的隐患。
4. 相机调用的避坑指南:四个真实翻车记录
4.1 Android 权限弹窗后直接闪退,logcat 抛出 SecurityException
现象:在 Android 6.0(API 23)真机上运行示例,首次点击「拍照」按钮,权限弹窗出现,点了「允许」,紧接着应用闪退回桌面,logcat 里出现 java.lang.SecurityException: Permission Denial。
原因:这只是配置不完整或者运行时权限请求和处理不一致导致的。很多时候是启用了运行时权限请求,但 Manifest 里没有注册对应的 Provider,或者 Android Support 库中的 FileProvider 配置缺失,导致相机应用无法把结果文件传回当前应用,权限被系统判定为非法访问后直接杀死进程。
解决:检查 Manifest 的 application 节点是否包含 FileProvider,并在 res/xml/file_paths.xml 里声明对外暴露的路径。示例项目里把 cache-path 指向了 ImagePicker 需要的临时目录,这个配置在 Android 7.0+ 上是强制要求,漏掉它你会得到上面这个闪退结果。另一种常见情况是权限请求回调与 Activity 生命周期错位,在 OnRequestPermissionsResult 还没回来时就去调用相机,也会偶发崩溃,建议把拍照调用放到权限回调确认之后。
4.2 iOS 上相机界面黑屏、然后应用退出
现象:iOS 真机调试,点击拍照按钮,画面一闪黑屏,然后应用直接退出到桌面,Xcode 的 Console 里没有任何 C# 异常输出。
原因:这个问题的根源几乎总是 Info.plist 缺少 NSCameraUsageDescription。iOS 的隐私策略是硬性的:应用调用摄像头 API 时,系统会查找 Info.plist 里对应的键,找不到就向系统发送终止信号,不给你写入异常日志的机会。因为这个错误发生在原生层,Xamarin.Forms 的托管异常捕获抓不到,看起来就像神隐崩溃。
解决:在 Info.plist 里补上 NSCameraUsageDescription 和 NSPhotoLibraryUsageDescription,然后把应用彻底杀掉重装。注意是重装,不是重新部署,因为 iOS 对 Info.plist 变更的检测在已有安装包上偶尔不刷新。我验证过多次,只要这两个键存在,黑屏问题就消失。要是你的应用在后续版本增加了录像功能,记得把 NSMicrophoneUsageDescription 也提前加上,否则会在用户点「录制」时崩溃。
4.3 竖着拍的照片,预览时转了 90 度
现象:一部 Android 坚屏手持拍摄,拍完返回的照片在 Windows 上打开显示正常,但在应用里 Image 控件展示时转了 90 度,横过来了。
原因:JPEG 文件里有一个 Exif 方向标签,记录了拍摄时设备的旋转角度。Android 原生相机返回的照片经常带 Orientation 标签,但你用 ImageSource.FromFile 直接加载时,Xamarin.Forms 的图片解析器不会读取这个标签,而 Windows 的照片查看器会自动识别方向,造成「系统里正常、应用里转」的怪相。iOS 的 MediaPicker 内部会在返回前按 Exif 方向重写像素数据,所以这个坑主要在 Android 端。
解决:读取图片的 Exif 信息,按方向标签手动旋转,再把结果保存回去。示例项目里在 CameraService 中有个 RotateIfNeeded 方法,思路是先用 MetadataExtractor 这类库读取 Orientation 值,0、90、180、270 分别做对应旋转,最后用 Bitmap.Compress 重新编码 JPEG。核心代码结构如下。
public static void RotateIfNeeded(string imagePath) { // 读取 Exif 方向标签,常见值为 3(旋转180度)、6(逆时针90度)、8(顺时针90度) int orientation = ReadExifOrientation(imagePath); if (orientation == 1 || orientation == 0) { return; // 无需旋转 } var bitmap = BitmapFactory.DecodeFile(imagePath); using (var matrix = new Matrix()) { if (orientation == 3) { matrix.PostRotate(180); } else if (orientation == 6) { matrix.PostRotate(90); } else if (orientation == 8) { matrix.PostRotate(-90); } var rotated = Bitmap.CreateBitmap(bitmap, 0, 0, bitmap.Width, bitmap.Height, matrix, true); using (var fs = new FileStream(imagePath, FileMode.Create)) { rotated.Compress(Bitmap.CompressFormat.Jpeg, 90, fs); } rotated.Recycle(); } bitmap.Recycle(); }这段代码只处理了最常见的三个方向值,完整的 Exif 方向有 8 种,如果你们的测试覆盖了传感器异常的设备,建议把镜像翻转也做进去。另一点要注意:Bitmap.DecodeFile 会把整个大图加载进内存,如果你在拍照路径已经用 PhotoSize.Medium 压缩过就没问题,但如果你改成了 Full 原图,记得用 BitmapFactory.Options 设置 InSampleSize 采样,否则内存直接爆。
4.4 连续拍照十几次后应用 OOM 崩溃
现象:在低端 Android 设备上反复拍照,每拍一次展示一张,到第 12 张左右,应用抛出 OutOfMemoryError,随后崩溃。
原因:这是典型的图片引用未释放问题。ImageSource.FromFile 每次设置新图,Image 控件的旧 Source 在没有手动置空时不会立刻释放;更隐蔽的是,你在 CameraService 里每多调用一次 CapturePhotoAsync 就多出一个临时文件,这些文件都存在 AppDataDirectory 里,没有清理策略。长时间运行后,内存和磁盘都被吃满。
解决:在拍照按钮的事件处理里,设置新图前先显式释放旧 Source,同时在服务层加一个文件清理方法,控制 AppDataDirectory 里图片文件的数量。示例项目里给出的是按时间戳命名,你可以顺手用上次消息队列里的 CreateFromUnixTimestamp 做过期判断,只保留最近十张的副本。另一个更简单的方案是在展示层之前用缩略图路径替换原图路径,比如用 FFImageLoading 的 DownSample 功能,它会在内部生成低尺寸图并回收原图,适合消息发送类的页面。
5. 示例的进阶用法:把 CameraView 自定义渲染用起来
5.1 什么时候 MediaPicker 不够用:自定义相机页面的两个信号
MediaPicker 解决问题的方式是调起系统相机,这有两个天然边界。第一,你无法在应用内定制相机界面,比如加一个「扫描文档」的框、顶部放一个闪光灯开关;第二,系统相机应用在部分定制 ROM 上可能非常臃肿,启动慢,用户等待时间长。当业务出现这两个信号中的任何一个,就该从 MediaPicker 迁移到自定义相机实现。
示例里没有包含完整的自定义相机 UI,但提供了 CameraPreview 控件的基础骨架,你可以理解为「占位页面」。一个合格的自定义相机方案需要哪几块?Android 端要用 Camera2 API 或 CameraX,把相机预览流绑定到 SurfaceView 或 TextureView 上;iOS 端要用 AVCaptureSession 配 AVCaptureVideoPreviewLayer;中间的 C# 层通过一个自定义 View 来统一两端接口,让页面的 XAML 像使用普通控件一样使用 CameraPreview。
5.2 自定义相机页面的 Renderer 骨架与生命周期控制
假设你要在共享项目里声明一个名为 CameraPreviewView 的自定义控件,最朴素的实现是让它在 Android 上承载一个 SurfaceView,在 iOS 上承载一个 CameraPreviewLayer 包装过的 UIView。关键在生命周期:相机的打开、预览、释放必须与页面的 OnAppearing、OnDisappearing 对齐,一旦错位,就会出现应用切到后台再返回时相机黑屏。
Android 端 Renderer 的骨架如下,代码重点是 OnElementChanged 里的释放逻辑,这部分是黑匣子最容易出幺蛾子的地方。
[assembly: ExportRenderer(typeof(CameraPreviewView), typeof(CameraPreviewRenderer))] namespace CameraDemo.Droid.Renderers { public class CameraPreviewRenderer : ViewRenderer<CameraPreviewView, Android.Views.View> { private CameraDevice _cameraDevice; private CameraCaptureSession _captureSession; protected override void OnElementChanged(ElementChangedEventArgs<CameraPreviewView> e) { base.OnElementChanged(e); if (Control == null) { // 创建 SurfaceView 并绑定到原生内容 var surfaceView = new SurfaceView(Context); SetNativeControl(surfaceView); } if (e.OldElement != null) { // 页面移除或控件被回收时必须释放相机,否则下一次打开时设备被占用 ReleaseCamera(); } if (e.NewElement != null) { // 打开系统相机并把预览流绑定到 SurfaceView OpenCamera(); } } private void OpenCamera() { /* Camera2 API 调用,需要处理权限 */ } private void ReleaseCamera() { _captureSession?.Close(); _cameraDevice?.Close(); _cameraDevice = null; } } }这段生命周期的顺序是:先释放旧元素关联的相机实例,再打开新元素需要的相机,顺序不能反。如果你先 OpenCamera 再 ReleaseCamera,会出现相机刚打开就被自己释放掉,页面黑屏但相机闪光灯却亮着的诡异状态。iOS 端对应的是 ViewDidAppear 和 ViewDidDisappear 生命周期,用 AVCaptureSession 的 StartRunning 和 StopRunning 做配对。
自定义相机方案的另一个重要参数是预览分辨率。MediaPicker 里你只需要指定 PhotoSize,但自定义渲染器里你必须自己决定 SessionPreset(iOS)或 SessionConfiguration 的尺寸(Android)。选 1920x1080 的预览和 1280x720 的预览,在画面延迟和清晰度上差别很大;以扫码为目标的业务用 640x480 就够,拍文档则需要 1920 以上,这个参数放到 Renderer 构造里用枚举控制即可。
6. 验证相机功能没白写:五个自查清单与最后一公里
6.1 五个检查点:从权限到图片路径
把示例项目跑通后,别急着认为任务完成。我建议你在真机上按下面五个点逐个验证,任何一个出问题,都说明你的集成方式有隐患。我用这个清单排查过多个实际项目,值得照着走一遍。
| 检查点 | 操作方式 | 预期结果 |
|---|---|---|
| 1. 权限弹窗 | 首次安装后点击拍照 | Android 和 iOS 各自弹出权限说明,文案与 Info.plist 一致 |
| 2. 取消返回 | 打开系统相机后按返回键 | 应用无闪退,Image 控件保持原图不变 |
| 3. 旋转方向 | 竖屏拍摄一张后查看图片 | 图片在应用内显示为正向,不横置 |
| 4. 连续拍摄 | 连拍 20 张并反复切换展示 | 无 OOM、无旧图残留 |
| 5. 后台恢复 | 拍完照片后把应用切后台,再切回来 | 相机功能仍能正常调用,无设备占用异常 |
第 5 项很关键但容易被忽略:Android 的相机资源在应用切后台后可能被系统回收,如果你的代码没有在 OnResume 里重新初始化相机实例,切回来再点拍照按钮会卡住。MediaPicker 方案相对安全,因为系统相机是独立应用,但自定义渲染器方案必须补全这个生命周期处理。
6.2 示例工程文件清单与替换方法
最后说这个示例文件怎么用到你自己的项目里。你需要拿走的是 CameraService.cs、MainPage 的调用代码片段,以及 Android / iOS 的权限配置。有了这三个部分,就等于有了一个完整的相机关联功能模块。
建议的做法是:把 CameraService.cs 放到项目的 Services 目录下,然后把你自己的页面里原来写 MediaPicker 调用的地方全部替换成 CameraService.CapturePhotoAsync()。这样做的好处是,以后你想把 MediaPicker 换成 CameraView 自定义实现,只需要改 CameraService 内部逻辑,不需要动页面层任何一行 XAML 代码。接口设计层面,CameraService 返回的是 string 路径,不是 Stream,这样页面层可以直接用 ImageSource.FromFile 绑定,也方便你直接把这个路径传给后端上传接口。
我有一次接手项目,发现团队直接在页面里写了二十多处 MediaPicker 调用,后来要求改为自定义相机,被迫把所有页面改了一遍。从那以后,我再接任何 Xamarin.Forms 项目,凡是涉及相机、相册、文件选择这类能力,都强制先在共享层建一个 Service 类做成统一出口,再开始写业务。这个习惯省下的时间比想象中多得多,也推荐你照着这样做。希望这篇拆解对你派得上用场。
本文还有配套的精品资源,点击获取