Delphi 10.2 Android摄像头调用:权限、Intent与图片落盘全流程 简介面向Delphi 10.2开发者的Android摄像头调用与控制完整示例包适合需要在FMX框架下快速实现拍照、录像、预览等功能的移动端项目。资源共92个文件包含pas源码、fmx界面、xml配置含AndroidManifest权限声明、so动态库、apk安装包及png截图等压缩包约45.78MB已有1206人学习。示例详细演示了从AndroidManifest.xml添加uses-permission权限、拖放TAndroidCamera组件、绑定Preview预览、编写OnTakePicture等事件处理到焦点、变焦、闪光灯控制及图像数据转换的完整流程并附有项目工程文件dproj/dpr和版本历史备份便于直接打开工程对照学习与调试。代码中考虑到设备兼容性、错误处理与用户交互设计可作为实际项目的基础框架进行二次扩展。1. 在 Delphi 10.2 里调用 Android 摄像头绕不开的三个底层环节用 Delphi 10.2 跑 Android 摄像头需求最容易遇到的现象是照抄网上代码权限弹窗也加了真机上一跑要么黑屏、要么拍完返回一张模糊小图、要么回调根本不走。问题不在 Delphi而在你把“摄像头组件”当成了黑匣子。一个合格做法是把它拆成三个底层环节Android 权限模型、系统相机 Intent、图片文件落盘路径。TTakePhotoFromCameraAction 只是把这三件事包了一层不是它自己打开了摄像头。这篇笔记来自某开发者把一个老 Delphi 项目移植到 Android 真机的完整过程适合用 Delphi 做工具类 App、或者刚把 FireMonkey 工程发布到安卓端的开发者。看完能直接抄一套可跑的权限申请与拍照回调代码还能避开五个真机常见坑。2. 两条主流路线TTakePhotoFromCameraAction 与 TCamera 预览组件的选型2.1 为什么优先 TTakePhotoFromCameraActionIntent 包装层的取舍在 Delphi 的 FireMonkey 体系里调用 Android 摄像头先要分清需求类型是“拍一张照片拿回来用”还是“在 App 内做持续预览、扫码、人脸框”。前者最合适的是 TTakePhotoFromCameraAction后者才轮到 TCamera 控件。TTakePhotoFromCameraAction 本质上不是摄像头 API它包装的是 Android 的ACTION_IMAGE_CAPTUREIntent。意思是你告诉系统“我想拍张照片”系统拉起相机应用拍完把结果交回你的 Activity。这个方案的优点是跨进程系统的相机应用处理了预览、对焦、曝光、白平衡你的 App 不用碰任何底层 Camera 接口也不用担心各家 ROM 的 Camera HAL 差异。缺点是它不给你连续视频帧只给你一张最终图片所以做不了扫码预览。我一般会根据三种场景选型场景推荐方案理由拍照后拿原图保存TTakePhotoFromCameraAction内部已处理多数 ROM 差异省事实时预览/扫码TCameraComponent有 SampleBufferReady 回调可逐帧处理对图片质量有极端要求手动 Intent FileProvider可完全控制保存路径与大小用 TTakePhotoFromCameraAction 时建议直接从组件面板拖到 Form 上不要在代码里 Create。因为它在 Android 平台要绑定当前 Activity 的生命周期设计期放上去Delphi 会在启动时自动把它的父 Activity 关联好。代码里手动 Create 也不是不行但要自己在 FormActivate 后调用绑定逻辑多一步少一步就会遇到“Execute 没反应”的怪问题。2.2 什么时候要用 TCamera连续预览和扫码场景如果你的界面需要一个取景框比如做一个二维码扫描器TTakePhotoFromCameraAction 完全不合适。FireMonkey 里对应的是 TCameraComponent。它在 Android 上封装了 Camera2 的上层接口设置好 Kind 和 Active 后摄像头画面会渲染到绑定它的组件上。常见做法是在 Form 上放一个 TCameraComponent 和一个 TImage然后这样启动预览procedure TFormMain.StartPreview; begin CameraComponent1.Kind : TCameraKind.Back; // 后置摄像头 CameraComponent1.Active : True; end; procedure TFormMain.CameraComponent1SampleBufferReady(Sender: TObject; const ATime: Int64); begin // 每帧回调这里可以拿 Bitmap 做分析也可以直接显示 CameraComponent1.GetBitmap(Image1.Bitmap); end;这段代码的关键是OnSampleBufferReady它在 Android 相机输出视频帧时被触发参数ATime是帧时间戳通常用来做隔帧处理比如扫码算法每 3 帧跑一次减少 CPU 开销。GetBitmap把当前帧拷贝到指定 Bitmap注意Image1.Bitmap需要预先设置尺寸否则第一次拷贝会因画布未初始化而慢一帧。需要特别说明的是Delphi 10.2 的 TCameraComponent 默认可能不在组件面板上要在安装示例组件后才出现。如果你只想做简单拍照不建议为它折腾安装流程。组件事件名在不同小版本里有过调整以你机器上控件面板显示为准逻辑不变。2.3 Delphi 10.2 的权限模型从清单申报到运行时申请Android 从 6.0API 23开始引入运行时权限权限分两类安装时权限和运行时权限。相机属于运行时权限存储读写在高版本上也同样运行时申请。Delphi 10.2 的 Project Options 里有一个 “Uses Permissions” 列表这里勾选的是写入 Manifest 的权限声明它解决的是“允许安装”不解决“运行时允许”。这就是为什么你勾了权限、装了应用一开拍照还是闪退或黑屏。缺的是运行时申请。Delphi 从 10.2 开始推荐用System.Permissions这个单元统一处理uses System.Permissions, Androidapi.JNI.Os, Androidapi.Helpers; function HasCameraPermission: Boolean; begin Result : PermissionsService.IsPermissionGranted( JStringToString(TJManifest_permission.JavaClass.CAMERA)); end;PermissionsService是单例入口IsPermissionGranted接收的是权限字符串。这里通过TJManifest_permission.JavaClass.CAMERA拿系统的权限常量避免手写字符串拼错。如果你的工程同时要写文件还要把WRITE_EXTERNAL_STORAGE一并检查只申请相机权限而存储权限没给拍完保存时会静默失败。权限模型这块想清楚之后就可以进入具体实现。3. 从权限到拍照落盘一套可复制的调用流程3.1 清单申报在 Project Options 里勾选相机与存储权限先打开 Project Options Uses Permissions把下面三项勾上android.permission.CAMERA android.permission.WRITE_EXTERNAL_STORAGE android.permission.READ_EXTERNAL_STORAGE如果同时要做实时预览还要再加android.permission.RECORD_AUDIOREAD_EXTERNAL_STORAGE主要用于 Android 低版本上读取相册文件如果你只拿拍照返回的路径不读相册也可以不勾。但勾上不影响功能还能省掉后续排查“为什么相册图片读不出来”的时间。这一步有几个人容易翻车直接手改AndroidManifest.xml。Delphi 每次编译会根据 IDE 设置重新生成 Manifest你手工加的uses-permission会被覆盖。所以权限声明一定要在项目选项里勾不要直接改模板文件。如果因为特殊需求必须手改改完之后把这个 Manifest 放到Project Android Manifest指定为自定义模板否则白改。3.2 运行时申请判断与请求的两段式写法清单申报之后代码里还需要在调用拍照前做运行时检查。推荐两段式先判断再请求。代码放在按钮事件里uses System.Permissions, Androidapi.JNI.Os, Androidapi.Helpers, FMX.Dialogs; procedure TFormMain.btnTakePhotoClick(Sender: TObject); begin if HasPermission(JStringToString(TJManifest_permission.JavaClass.CAMERA)) and HasPermission(JStringToString(TJManifest_permission.JavaClass.WRITE_EXTERNAL_STORAGE)) then begin DoTakePhoto; end else begin PermissionsService.RequestPermissions( [JStringToString(TJManifest_permission.JavaClass.CAMERA), JStringToString(TJManifest_permission.JavaClass.WRITE_EXTERNAL_STORAGE)], procedure(const APermissions: TArraystring; const AGrantResults: TArrayTPermissionStatus) begin if (Length(AGrantResults) 0) and (AGrantResults[0] TPermissionStatus.Granted) then DoTakePhoto else ShowMessage(相机权限被拒绝无法拍照); end); end; end; function TFormMain.HasPermission(const APermission: string): Boolean; begin Result : PermissionsService.IsPermissionGranted(APermission); end;这段代码里RequestPermissions接收两个参数权限数组和回调匿名方法。回调里AGrantResults与请求权限按顺序对应所以AGrantResults[0]是相机权限的结果。TPermissionStatus.Granted表示用户点了允许。注意 Android 还有一个“仅本次允许”的状态Delphi 10.2 里通常也映射为 Granted按允许处理即可。这里为什么要两组权限一起申请因为相机权限只是拍照写存储权限决定图片能不能落盘。如果分开申请用户第一次拒绝第二个弹窗图片保存失败排查半天还以为组件坏了。一次申请两个是最省心的写法。3.3 触发拍照与 OnDidFinishTaking 回调权限拿到之后就是调用动作。假设你在 Form 上放置了 TTakePhotoFromCameraAction命名为 TakePhotoFromCameraAction1代码是这样的procedure TFormMain.DoTakePhoto; begin TakePhotoFromCameraAction1.TargetDirectory : TPath.GetDocumentsPath; TakePhotoFromCameraAction1.NeedSaveToAlbum : True; TakePhotoFromCameraAction1.OnDidFinishTaking : DoDidFinishTaking; TakePhotoFromCameraAction1.Execute; end; procedure TFormMain.DoDidFinishTaking(ASavedPhoto: TCustomBitmap); begin if ASavedPhoto.IsEmpty then begin ShowMessage(没有拿到照片数据请检查路径与权限); Exit; end; Image1.Bitmap.Assign(ASavedPhoto); // 如果需要保存原图这里从 ASavedPhoto 拿不到文件路径 // 要另存一份或者使用 SavedPhoto 属性见 3.4 end;TargetDirectory表示拍完照临时文件的存放目录。踩坑点在于不要用TPath.GetDocumentsPath以外的路径。在 Android 上私有目录外的路径可能没有写权限或者被系统的文件访问策略拦截。NeedSaveToAlbum : True表示同时把照片写入系统相册这是 Android 上必须的。很多人问不设 True 行不行在 iOS 上可以在 Android 上设 False 时某些机型会返回空 Bitmap因为系统相机应用无法把路径回传。所以 Android 端一律 True。OnDidFinishTaking回调里拿到的是已经解码的 Bitmap直接用Assign显示。如果图片很大Assign本身开销不高但后续内存压力大这个在第 4 章避坑里详细说。3.4 文件路径确认与相册刷新Delphi 10.2 的 TTakePhotoFromCameraAction 在 Android 上拍照完成后照片文件存在两个地方一是 TargetDirectory 里的临时文件二是相册目录如果 NeedSaveToAlbum 为 True。组件在回调里没有直接暴露文件路径属性但你可以通过判断临时目录里最新文件来拿到function GetLatestPhotoPath(const ADirectory: string): string; var LFiles: TStringDynArray; LSearchRec: TSearchRec; LMaxTime: Integer; LNewestFile: string; begin LMaxTime : 0; LNewestFile : ; if FindFirst(ADirectory PathDelim *.jpg, faAnyFile, LSearchRec) 0 then begin repeat if (LSearchRec.Time LMaxTime) and (LSearchRec.Name .) and (LSearchRec.Name ..) then begin LMaxTime : LSearchRec.Time; LNewestFile : ADirectory PathDelim LSearchRec.Name; end; until FindNext(LSearchRec) 0; FindClose(LSearchRec); end; Result : LNewestFile; end;这段代码是 Delphi 原生的文件遍历写法FindFirst匹配*.jpg按修改时间挑出最新那个。因为每次拍照时间不同最新文件基本就是刚拍的那张。这个方法有个前提临时目录里不要混入其他照片否则可能拿错文件。拿到路径后如果需要刷新系统相册可见性可以在 Android 上发一条媒体扫描广播uses Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes, Androidapi.JNI.Net, Androidapi.Helpers, FMX.Platform.Android; procedure RefreshMediaScan(const AFilePath: string); var LIntent: JIntent; begin LIntent : TJIntent.JavaClass.init( StringToJString(android.intent.action.MEDIA_SCANNER_SCAN_FILE)); LIntent.setData( TJNet_Uri.JavaClass.parse(StringToJString(file:// AFilePath))); SharedActivity.sendBroadcast(LIntent); end;这条广播告诉系统“这个文件是图片请更新媒体库”否则相册 App 里可能看不到。它不影响你 App 内直接读文件所以只在需要用户去相册查看时调用。4. 真机避坑五个 Android 摄像头调用问题的排查记录4.1 权限弹窗压根没出现拍照直接失败现象应用安装后点击拍照按钮没有权限弹窗或者闪一下黑屏然后回到界面日志里有 SecurityException。原因多数情况是清单权限没声明。Delphi 的 Project Options 里没勾 CAMERA运行时权限调用就找不到对应权限声明系统直接跳过弹窗。另一种情况是 targetSdk 版本很低低于 23运行时权限不会触发但你又调了 RequestPermissions结果就是个空操作。解决到 Project Options Uses Permissions重新勾选相机与存储权限Clean 一次再 Build。不要只勾了相机权限而漏掉存储权限。targetSdk 低的确认 Project Options Android 里的 SDK 设置没有为了兼容老设备刻意压到 22 以下否则干脆不做运行时申请直接调用 Execute 也行。4.2 拍完图片糊成一团拿到的其实是缩略图现象照片能拍回调也有数据但 Image1 显示的只有几百像素放大了糊。文件路径里存的原图却是好的。原因这是最经典的 Intent 缩略图问题。ACTION_IMAGE_CAPTURE的一个行为是如果你的代码没有给 Intent 指定EXTRA_OUTPUT系统相机应用只会用 Bitmap 方式传回一张低分辨率缩略图存放在 result data 里。Delphi 的 TTakePhotoFromCameraAction 内部默认行为在旧版本上正好踩这个分支。解决设置NeedSaveToAlbum : True让 Delphi 强制走文件落盘路径。如果已经是 True 还拿到小图检查回调里是否用错了数据来源只用了 data 而没读文件。我之前就见过一段代码回调里从ASavedPhoto直接Assign但实际它已经是被解码过的原图问题出在图片显示控件被拉伸了跟图片本身无关。先看文件实际尺寸再判断。4.3 某些机型回调不触发拍照成功但没后续现象在 A 手机上一切正常换到某品牌手机上相机应用拍完界面跳回 App但 OnDidFinishTaking 没走Image1 没变化。原因这台机器系统相机应用把结果 Activity 返回时带了额外标志或者它在保存图片后立刻杀掉了中间进程导致上层 Activity 恢复时回调参数为空。这种问题在国产 ROM 上概率高本质是系统相机与 Delphi Activity 生命周期冲突。解决不回调试试点按钮在 Form 的OnActivate或相机的OnSelectedActivity里判断临时目录的最新文件变化。拍照按钮触发后记录当前最新文件时间Form 重新激活时取最新文件时间变了就当作拍照成功。这个兜底方案不依赖回调路径确认方式用 3.4 里的GetLatestPhotoPath。注意不要把这个兜底逻辑写进回调里否则会双重复位。4.4 照片方向躺倒EXIF Orientation 的锅现象竖着手机拍的照显示出来是横着的像是被转了 90 度。在部分 ROM 上正常部分不正常。原因Android 相机输出的 JPEG 文件头部有 EXIF 的 Orientation 字段系统相册会读它来自动转正。但 Delphi 组件解码 Bitmap 时不一定处理这个字段已解码的 Bitmap 没有方向信息于是显示就躺下了。解决读取文件后按 EXIF 方向旋转。Delphi 原生没有直接的 EXIF 读取单元常见做法是用 JEDI 的 JpegExif 单元或者用 Android 的 ExifInterfaceuses Androidapi.JNI.Media, Androidapi.JNI.JavaTypes, Androidapi.Helpers; function GetExifOrientation(const AFileName: string): Integer; var LExif: JExifInterface; begin LExif : TJExifInterface.JavaClass.init(StringToJString(AFileName)); Result : LExif.getAttributeInt( StringToJString(Orientation), 0); end;拿到值后1不动3转 180 度6转 90 度8转 270 度。判断完再对 Bitmap 做旋转。如果很多图片方向都正常这个预处理可以只加在保存原图流程里。4.5 大图 OOM 闪退现象低内存机型上拍照完成一瞬间 App 直接闪退或者返回界面时卡了几秒然后被系统杀掉。原因现在手机相机拍出的照片动辄 4000 万像素Bitmap 解码后每像素按 4 字节算一张 4000×3000 的图就要 48MB 内存。在 Java 堆上加上已有界面的 BitmapOOM 不算意外。Delphi 的 ARC 机制会释放但释放时机不受你控制高峰期并存两张图就直接爆。解决加载显示图时做缩放采样。不要拿原图 Bitmap 直接 Assign 到 Image用一个按屏幕尺寸解码的 Bitmapuses System.UITypes, FMX.Graphics; function LoadScaledBitmap(const AFileName: string; const AMaxSize: Integer): TBitmap; var LSource: TBitmap; begin LSource : TBitmap.CreateFromFile(AFileName); try Result : TBitmap.Create( Round(LSource.Width / LSource.Height * AMaxSize), AMaxSize); Result.Canvas.DrawBitmap(LSource, RectF(0, 0, LSource.Width, LSource.Height), RectF(0, 0, Result.Width, Result.Height), 1); finally LSource.Free; end; end;AMaxSize按屏幕高度传比如 1920。这样显示图内存一下就下来。原图仍然走 3.4 的文件路径保存原始数据不丢。从那以后我每次拍照 App 都会把“显示”和“保存”分成两条路决不让原图长期驻留内存。5. 进阶锁方向、原图清理与真机验证清单拍照类应用最让人恼火的体验是竖屏拍完相机应用回来后整个界面重新创建了一次旋转动画看了两秒刚才的状态全没了。这是 Android 的 Activity 在屏幕方向变化时的重建行为。Delphi 应用中应对方法有两种其一Project Options Android 里把 Orientation 固定为 Portrait 或 Landscape其二代码里给 Form 的Orientation属性赋值。procedure TFormMain.FormCreate(Sender: TObject); begin Orientation : TFormOrientation.Portrait; end;这句在 Android 上会锁定当前窗体的显示方向拍照拉起系统相机后返回时不会走重建。如果你确实需要横竖屏都支持那只能接受回调丢失的现实把拍照结果直接从文件路路径恢复而不是依赖回调参数。最后给一份真机验证清单。每换一台新设备照着走一遍能省大量排查时间项目验证方法预期结果权限声明安装后点击拍照按钮出现系统权限弹窗权限拒绝弹窗选拒绝再点拍照有提示文案不闪退正常拍照允许后拍一张OnDidFinishTaking 触发原图分辨率查看保存文件宽高不低于相机默认分辨率相册可见打开系统相册刚拍的照片出现在相册方向正确竖屏拍摄室内照片显示不躺倒低内存连续拍 20 张无 OOM 崩溃回调异常替换一台国产 ROM 真机兜底文件检测生效这套清单也是我自己后来强制走的流程每个 Delphi 摄像头项目交付前都要完整过一遍。前两次做的时候第 4 项、第 6 项必翻车到现在才稳。希望帮到你这份工程配套的源码包里已经包含了上面所有代码复制过去改个包名就能直接用。本文还有配套的精品资源点击获取