资讯动态

Delphi 10.2 调 Android 摄像头:权限与回调全链路解析

发布时间:2026/10/21 13:59:44 来源:尧图企业网站定制
简介面向Delphi 10.2开发者的Android摄像头调用示例工程依托FireMonkey框架解决移动端拍照、录像、实时预览及相机控制等核心需求是移动开发中常见的功能场景。工程覆盖完整开发链路从AndroidManifest权限声明、TAndroidCamera组件属性配置到OnTakePicture等事件编写、预览画面与TImage控件绑定、JPEG图像数据转为TBytes、变焦与闪光灯控制再到设备兼容性检查、交互UI设计与错误处理等实战要点适合具备Delphi基础、开始涉足Android原生功能调用的开发人员参考。压缩包共含92个文件以fmx界面文件、pas单元源码、xml配置和dproj工程文件为主另附png截图与apk安装包整体大小45.78MB文件内保留多版本历史备份便于对照代码演进过程。目前已有1206人学习下载。借助这套完整示例读者可直接打开工程运行调试免去权限声明和组件配置的踩坑过程清晰掌握摄像头调用的完整流程还能基于现有源码快捷扩展前后摄像头切换、自动对焦、录像时长控制等功能同时为学习FMX框架与Android原生API交互提供绝佳范例。1. 用 Delphi 10.2 调 Android 摄像头权限和回调是绕不开的两座山一个从 VCL 时代一路写 Delphi 的开发者第一次把工程切到 Android 目标平台满心以为写一句Camera.TakePhoto就能完事。真机上一跑App 闪退日志里只有一行SecurityException后台一查连权限都没申请。就这一个小小的缺口能卡住一个熟练的 Windows 老手。这篇项目笔记拆的正是这么一套东西在 Delphi 10.2 里用系统相机 Intent 完成拍照、拿回原图、压缩入库顺带把相册选择也接进来。核心不是摄像头本身而是动态权限申请和 Activity 回调的完整链路适合要往 Android 平台迁移 Delphi 业务模块的开发者参考。2. 工程骨架与权限配置先讲清楚 10.2 的 Android 侧到底有什么讲究2.1 工程文件结构.dpr、.dproj 和 AndroidManifest 模板各管什么Delphi 10.2 生成的 Android 工程表面上看还是那几个熟悉的文件但实际分工有差异。.dpr是程序入口负责 Application.Initialize 和 Form 创建.dproj是真正的工程文件里面存放 Android 相关的版本号、架构Arm/Intel、包名、Activity 名称这些配置.pas单元文件则是业务逻辑所在。和纯 Windows 工程最大的一处不同是Android 工程里有一个AndroidManifest.template.xml它在编译时会被 Delphi 自动合并、替换占位符最后生成应用真正使用的AndroidManifest.xml。很多从 Win32 转过来的开发者在 AndroidManifest 上踩的坑都一样改了源文件目录下的AndroidManifest.xml保存后用 Delphi 重新编译发现改动没了。因为 Delphi 每次编译都会根据模板重新生成该文件正确做法是改Project Options Android Uses Permissions页面或者直接编辑AndroidManifest.template.xml。这是我第一次做 Android 相机项目时就被上了一课的地方。清单文件里必须声明几项权限最基本的组合是CAMERA、READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE。在 10.2 中手动编辑模板文件才是可控的做法因为选项页面上对权限的收纳并不全。典型的一段模板配置如下uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE /这三行缺了任何一个相机界面都可能打得开但保存图片失败。原因是目标 SDK 版本与系统安全策略不一致时的表现非常隐蔽不会直接报权限异常而是静默产生一个空文件。别问我怎么知道的我花了一下午排查为什么照片总是拍完就消失。2.2 权限双轨机制清单声明只是第一步运行时动态申请才是真正绕不过的门槛Android 6.0 之后权限系统改成了运行时申请。清单文件里的uses-permission不再代表用户一定会授予权限弹窗需要在代码里主动触发用户允许后才能调用受保护的系统能力。这个机制对长期在 Windows 平台开发的人来说很难适应因为 Windows 桌面开发中权限通常安装时就定好了。Delphi 10.2 提供了一个封装的入口TPermissionsService通过全局函数PermissionsService访问。它本质上是对requestPermissionsJava 接口的 Delphi 包装。调用方式不复杂但重载方法多新手容易对着智能提示发呆。我建议第一次测试时只保留CAMERA一个权限后面再逐步加存储权限减少调试面的干扰。针对拍照场景常见的权限申请写法我一般拆成一个独立函数返回一个布尔值表示是否已全部授权避免每次进入拍照流程都弹窗function CheckAndRequestCameraPermission: Boolean; var PermissionsList: TArraystring; GrantResults: TArrayTPermissionStatus; begin PermissionsService.CurrentPermissions; PermissionsList : TArraystring.Create( JStringToString(TJManifest_permission.JavaClass.CAMERA), JStringToString(TJManifest_permission.JavaClass.WRITE_EXTERNAL_STORAGE) ); PermissionsService.RequestPermissions( PermissionsList, 1024, procedure(const APermissions: TArraystring; const AGrantResults: TArrayTPermissionStatus) var I: Integer; AllGranted: Boolean; begin AllGranted : True; for I : Low(AGrantResults) to High(AGrantResults) do if AGrantResults[I] TPermissionStatus.PermissionGranted then AllGranted : False; if AllGranted then TThread.Synchronize(nil, procedure begin // 这里才真正启动相机 end ); end, procedure begin // 用户选了拒绝提示并返回 end ); end;第四行的JStringToString负责从 JNI 字符串转到 Delphi 字符串TJManifest_permission.JavaClass.CAMERA则是对 Java 清单常量的 Direct 引用。代码逻辑很简单先读当前权限列表再发起申请最后一个匿名过程用于接收回调如果用户拒绝则走第二个回调。要特别说明的是在这类回调里不能直接创建 Form 或者操作 UI 控件必须通过TThread.Synchronize回到主线程执行否则偶现的界面卡顿问题会让你怀疑人生。3. 核心模块拆解一次拍照从按钮到图片落地的完整链路3.1 启动系统相机为什么 Intent 方案比摄像头组件更实用Delphi 10.2 的组件面板里确实有原生摄像头相关组件但是封装度低底层依赖平台实现换一台机型可能就黑屏。真要做一个能拿到原图、体积可控的拍照流程用系统相机 Intent 是更稳的路线。系统相机 App 由各厂商预装它们的实现在大多数 Android 设备上经过充分验证我们只需要拼好两个东西ACTION_IMAGE_CAPTURE动作和照片保存的 Uri。function CreateCameraIntent(const AOutputFilePath: string): JIntent; var FileObj: JFile; UriObj: JUri; begin Result : TJIntent.Create; Result.setAction(StringToJString(android.media.action.IMAGE_CAPTURE)); FileObj : TJFile.JavaClass.init( TAndroidHelper.Context.getExternalFilesDir(StringToJString()), StringToJString(ExtractFileName(AOutputFilePath)) ); UriObj : TJUri.JavaClass.fromFile(FileObj); Result.putExtra(StringToJString(output), TJParcelable.Wrap(UriObj)); end;getExternalFilesDir拿到的目录属于 App 私有的外部存储目录不需要额外申请写存储权限这是很多教程里没讲清楚的一个差异点。照片输出路径如果指定为公共相册目录就必须在运行时申请写权限否则拍照回来的文件为零字节。参数里还有一个容易被忽略的点putExtra的 key 是字符串output。它实际上对应MediaStore.EXTRA_OUTPUT但在 Delphi 10.2 里直接引用TJMediaStore.ACTION_IMAGE_CAPTURE这类常量在部分小版本中编译不过我习惯退一步直接写字符串并加注释说明这是 Android 官方字符串常量避免不同版本的行为差异影响编译。3.2 拍照结果回调onActivityResult 悬案与事件挂接方式Intent 方案核心的难点来了——startActivityForResult发出去之后照片结果怎么回到 Delphi在 10.2 版本里这件事远不如 Windows 下那么简单系统相机是一个独立的应用你的 App 会在后台等待它产出结果。结果通过 Android 的 ActivityResult 机制返回但 Delphi 10.2 没有像 10.3 那样把onActivityResult直接在TAndroidHelper上层暴露出来。这个坑当年困扰了很多人论坛里讨论到几十楼才确认一个可用姿势通过TAndroidHelper.Activity的底层活动对象挂接事件。活动对象本身实现了JActivity接口实例类型是TAndroidActivity它有onActivityResult事件成员。用 Delphi 的TObject强转访问到该实例再绑定事件即可接收返回结果。procedure TForm1.FormCreate(Sender: TObject); begin FCurrentRequestCode : 0; (TObject(TAndroidHelper.Activity) as TAndroidActivity).onActivityResult : procedure(requestCode: Integer; resultCode: Integer; data: JIntent) begin HandleActivityResult(requestCode, resultCode, data); end; end;这里的强转原理是TAndroidHelper.Activity返回一个 JNI 接口引用但底层对象是一个 Delphi 对象实例因此强转为TAndroidActivity是安全操作。如果读者的 10.2 版本里该类型没有暴露onActivityResult事件最稳妥的兜底方案是自己写一个 Activity 的子类并覆盖该方法那会让工程复杂度提升一个量级所以这里优先介绍这个简约方案。3.3 从回调到 Bitmap原图路径处理与内存控制的临界点回调处理是这次拆解的重头戏因为你不是只需要把 Bitmap 展示到界面上还要考虑内存。系统相机拍照返回原图一张 4000 万像素照片直接解码成 Bitmap内存超过 200MB低端机上必挂。我用压缩参数inSampleSize来控制这是 Android 官方推荐的做法。procedure TForm1.HandleActivityResult(requestCode: Integer; resultCode: Integer; data: JIntent); var ImagePath: string; Bitmap: JBitmap; Options: JBitmapFactoryOptions; begin if requestCode FCurrentRequestCode then Exit; if resultCode TJActivity.JavaClass.RESULT_OK then begin if FIsFromPicker then ImagePath : GetRealPathFromUri(data.getData) else ImagePath : FCurrentImagePath; Options : TJBitmapFactoryOptions.JavaClass.init; Options.inJustDecodeBounds : True; TJBitmapFactory.JavaClass.decodeFile( StringToJString(ImagePath), Options); Options.inSampleSize : 2; while (Options.outWidth div Options.inSampleSize) 2048 do Options.inSampleSize : Options.inSampleSize * 2; Options.inJustDecodeBounds : False; Bitmap : TJBitmapFactory.JavaClass.decodeFile( StringToJString(ImagePath), Options); ImageView1.Bitmap.Assign(Bitmap); Bitmap.recycle; end; end;这里的关键参数inJustDecodeBounds先为真只读取图片宽高而不加载像素数据用得到的宽高计算合适的压缩倍率inSampleSize取 2 的幂每次翻倍直到图片宽度低于目标阈值。inSampleSize 4意味着图片尺寸缩小到四分之一内存占用大约只剩十六分之一。注意最后的recycle是立即释放 JBitmap 底层像素这在高频拍照场景里是救命操作否则十几个循环下来内存必然告急。这样处理虽然损失部分细节但应付手机相册预览和后续的二次加工已经够用。3.4 相册选择复用同一套回调难题的取巧实现相册选择的业务逻辑和拍照高度重叠代码层面稍微拧一下就能共用。只需要将FIsFromPicker置为 True然后用ACTION_PICK发起选择相册返回的data.getData()是一个 content:// URI需要解析成真实文件路径才能交给 BitmapFactory。function TForm1.GetRealPathFromUri(ContentUri: JUri): string; var Cursor: JCursor; ColumnIndex: Integer; begin Cursor : TAndroidHelper.Context.getContentResolver.query( ContentUri, nil, nil, nil, nil); if Cursor nil then Exit(StringToUTF8String(ContentUri.getPath)); Cursor.moveToFirst; ColumnIndex : Cursor.getColumnIndex( StringToJString(_data)); Result : JStringToString( Cursor.getString(ColumnIndex)); Cursor.close; end;核心逻辑是拿_data字段。系统相册的 URI 有各种形态有的自带文件路径有的必须走内容解析部分国产 ROM 还会对_data字段做限制所以我在尾部做了一个兜底直接返回 URI 的路径。读者如果在这里遇到Causes a negative value这类异常多半是Cursor未空判断导致的仔细检查空指针问题会发现大部分解析失败都能提前规避。4. 常见问题排查Delphi 10.2 摄像头开发的五个翻车点4.1 运行时权限弹窗没出现程序直接闪退现象在真机上点击拍照按钮App 没有任何反应Logcat 里出现java.lang.SecurityException: Permission Denial。原因清单文件里权限声明齐全但代码层没有触发运行时申请。Android 6.0 之后的机型必须走一次弹窗流程。解决确保拍照入口前调用PermissionsService.RequestPermissions而且在权限回调结束后再启动相机 Intent。我自己调试时还会先卸载 App 再重新安装因为权限一旦拒绝过弹窗在部分 ROM 上会短暂失忆实则是记忆了拒绝状态重装是最简单的重置手段。4.2 拍完返回空白 Bitmap现象拍照正常完成照片也按路径保存了但回到 App 后 ImageView 显示一片黑或白。原因照片未被保存到我们通过putExtra指定的输出路径而是直接以缩略图形式返回到了data中一些相机应用对大图输出支持不佳时会退回缩略图模式data.getExtras().get(data)拿到的是低分辨率预览图。解决不要依赖data的缩略图必须读取自定义路径下的文件再解码。如果该路径下文件仍为空多数是参数putExtra(output, ...)里面的 Uri 类型被传错了Delphi 里要包装成JParcelable这部分代码我在 3.1 中已经标注。4.3 照片方向是横的照片一旋转内存爆掉现象竖屏拍摄的照片在相册里是正的回到 App 里变成了横的翻转显示后又出现明显卡顿。原因系统相机把照片的旋转信息写入 EXIF 元数据而不是直接对像素做旋转变换解码时没有读取该信息就会得到横图。同时旋转 Bitmap 需要新建一个同样大小的位图内存开销直接翻倍。解决解码前先读取 EXIF orientation 字段再按角度旋转旋转过程用一个中间变量接收新 Bitmap 并立即释放旧 Bitmap避免两个大对象同时驻留内存。4.4 Android 7.0 之后 file:// 被拒的问题现象调用相机时在某些新机型上直接崩FileUriExposedException日志指向file://格式的 Uri。原因Android 7.0 开始App 对外暴露file://路径会被系统拦截。解决两种路径一是继续使用getExternalFilesDir私有目录配合FileProvider生成content://格式的临时候选 Uri这在 Delphi 中需要额外引入 JAR 支持二是我实际更常用的方案——好在这类需求只用于自己 App 内部处理直接让相机把照片写到应用私有的getExternalFilesDir目录不强制使用file://暴露给其他应用即可避开大部分 FileUriExposedException。4.5 模拟器上报错真机却正常现象Android 模拟器上点击拍照按钮返回 RESULT_CANCELED甚至直接没有反应把同一套 APK 装到真机却一切正常。原因模拟器中未必预装系统相机应用或者默认相机应用版本过低不支持指定的输出路径参数。解决调试时先检查模拟器是否安装相机 App如果确实没有就用adb shell确认ACTION_IMAGE_CAPTURE是否有可用的处理者更高效的做法是模拟器上只验证权限流程照片逻辑直接连真机跑省掉无谓的排查时间。5. 二次开发把拍照逻辑封装成抽象类一劳永逸前期踩完坑后续业务里还要复用这套逻辑得直接把粗糙代码重构成抽象类。我会建议封装一个TCameraHelper对外只暴露两个方法TakePhoto(AOnFinish: TProcTBitmap)和PickFromGallery(AOnFinish: TProcTBitmap)。内部统一维护当前请求码、图片路径、压缩比例等状态这样业务层调用方不用关心 Activity 回调细节。TCameraHelper class private FOnCameraResult: TProcTBitmap; FImagePath: string; FRequestCode: Integer; procedure InternalHandleResult(requestCode, resultCode: Integer; data: JIntent); public constructor Create; procedure TakePhoto(AOnSuccess: TProcTBitmap); procedure PickFromGallery(AOnSuccess: TProcTBitmap); end;TakePhoto内部执行路径是检查权限 → 构造 Intent → 设置输出路径 → 发起startActivityForResult然后统一走InternalHandleResult。对外暴露的回调中直接返回处理好的 Bitmap调用方不需要再知道 EXIF 是哪个角度、压缩比是多少真正把复杂逻辑收敛起来。我的习惯是连失败路径也设计好TProcTBitmap只处理成功场景失败场景单独一个TProcstring传入错误信息这样业务层能明确知道自己拍的照片到底是拍成了还是没拍成。判断失败的标准包括权限拒绝、用户取消、返回空文件三个状态。回调里统一弹 Toast 给用户反馈避免业务代码到处散落通知。从第一次运行时权限弹窗闪退开始到后面把相册和拍照的路径解析全部服帖最大的教训就是别把摄像头调用当成一个孤立控件来看。要把它当成一条完整链路权限、Intent、Uri、回调、解码压缩每个环节在 Android 系统升级后都可能翻车。从那以后我每次升级 Delphi 小版本都会盯着 Androidapi.JNI.App 和 Androidapi.Helpers 这两个单元看一遍 diff确认回调 API 有没有被调整。这套思维方式让后续的扫码、文件选择一类模块全都顺延下来。希望这篇拆解能帮你少走一段弯路也欢迎在按这套思路调试时对照自己的现象确认具体是哪一环出的问题。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑