☰
Delphi 10.2 Android摄像头调用实战:动态权限与预览封装避坑指南
2026/10/20 13:20:29 网站建设 项目流程

简介:面向 Delphi 10.2 开发者的 Android 摄像头调用示例工程,针对在 FireMonkey(FMX)框架下接入原生摄像头 API 的需求,提供了从 Android 权限声明、TAndroidCamera 组件配置到拍照、录像、实时预览与图像数据转换的完整实现思路。压缩包共 92 个文件,约 45.78MB,主要包含 Delphi 工程源码(.pas 与 .fmx 界面)、Android 配置清单(AndroidManifest.template.xml、.dproj)、编译产物(.apk、.so、.dex)、界面截图(.png)以及历史备份文件,各类型文件用途直观,便于按模块查阅。已有 1206 人浏览学习,适合具备一定 Delphi 基础、希望在 Android 端快速落地摄像头功能的移动开发人员。通过示例可掌握 TAndroidCamera 组件的使用、摄像头权限配置、事件处理、变焦与闪光灯控制等关键知识点;同时附带可运行 APK 和完整项目文件,帮助读者在实际设备上直接验证效果、排查兼容性问题。另外,工程中包含多个版本的历史备份,便于对照开发期间的代码变更,有助于快速定位问题。

1. 为什么 Delphi 10.2 调 Android 摄像头会翻车?这份资源把权限和预览都做了封装

用 Delphi 10.2 开发 Android 摄像头功能,最典型的体验就是“玄学”:同一套代码在模拟器上正常,换到真机黑屏;权限明明在清单里写了,一运行时还是闪退;好不容易拍到一张照片,却发现图像旋转了 90 度。这背后其实是 Android 6.0 动态权限、FireMonkey 窗体生命周期、以及底层 Camera API 三方叠加的复杂度。我拆的这份资源不是一台演示机,而是一个可直接编译运行的摄像头调用 Demo,重点是它把动态权限请求、相机预览、拍照保存、相册选择、甚至 JNI 回调都封装成了独立单元。适合刚接触 Delphi 移动端的开发者直接拿来做模板,也适合已经在做上位机或工具类 App 的人做二次改造。

2. 环境与选型:CameraComponent 还是 JNI 直调?把三条路线的参数差异说透

在 Delphi 10.2 里调 Android 摄像头,有至少三条路线:用 FireMonkey 自带的TCameraComponent,用 Intent 跳转系统相机,直接用 JNI 封装 Android 原生的Camera类。这三条路线的复杂度和可控性差别很大,选错了后面全是坑。

2.1 三条技术路线的对比与选型依据

TCameraComponent是 Delphi 10.2 自带的一套封装,它做的事情是帮你在 FMX 窗体上挂一个预览视图,并且把拍照结果以一种高层的TBitmap回调给你。它的最大优点是代码量极少,一个页面从创建到出图不超过 20 行;缺点是它依赖 FMX 的TCaptureCameraView组件,而TCaptureCameraView在布局上必须放在 Form 上,无法自由嵌到某个自定义控件的内部。同时它的对焦、曝光、闪光灯控制能力非常有限,你只能设置几个枚举值,比如TFocusMode.ContinuousAutoFocus、TTorchMode.TorchOn,但无法像原生 API 那样单独设置测光区域或帧率。

Intent 跳转系统相机是第二种路线,通过TIntent拉起MediaStore.ACTION_IMAGE_CAPTURE这个 Activity,得到一张系统相机拍完的照片。它的好处是不需要自己处理预览画面,所有对焦、闪光灯都由系统自带相机处理,稳定性和兼容性最好;坏处是照片返回的路径和分辨率不可控,在部分机型上返回的可能只是一个 512 像素的缩略图。要拿原图,必须传MediaStore.EXTRA_OUTPUT指定一个自己预先创建的FileProvider路径,这个过程在 Delphi 里没有现成封装,需要自己写 JNI 也不省事。

第三条路线是 JNI 直调 Android 原生Camera类。这条路线的代码量最大,但能拿到原始预览帧数据、YUV 数据,适合做二维码扫描、实时视频处理这类高级需求。它的核心问题是线程模型:Camera.open()之后,PreviewCallback会在 Binder 线程回调,这个线程不能直接碰 FMX 控件,否则必崩。很多 Delphier 第一次写 JNI 回调都栽在这里。

我的选型建议是:如果你的需求就是简单的拍照、拍完存文件、相册里再选一张,优先用第一种路线,也就是TCameraComponent配合TCaptureCameraView,这份资源也是这么写的。只有当你明确要逐帧分析图像,或者要做流媒体上送时,再考虑第三条 JNI 路线。下表是三条路线的核心差异:

路线优点缺点适用场景
TCameraComponent代码短、无需 JNI、和 FMX 配合自然控件布局受限、参数少、无法获取原始帧拍照、取景、上传
Intent 系统相机稳定性极高、免费获得全功能相机拿不到预览帧、路径处理繁琐、部分机型缩略图只取最终照片
JNI 直调 Camera完全控制、帧数据可编程线程复杂、代码多、调试难扫码、直播、人脸识别

2.2 权限与清单配置:Android 6.0+ 动态权限的关键点

Android 6.0 也就是 API 23 之后,摄像头和存储权限变成了运行时权限。也就是说,你在AndroidManifest.xml里写入的uses-permission只代表“安装时声明”,在运行时必须调用权限请求接口。Delphi 10.2 里做这件事需要用到System.Permissions单元,它封装了权限请求和回调,但很多人不知道的是,AndroidManifest.template.xml里的uses-feature也要写上,否则在部分平板上会直接认为设备不支持摄像头而无法安装。

先看清单配置。在 Delphi 10.2 中,Android Manifest 不是直接编辑最终文件,而是编辑工程里的AndroidManifest.template.xml。你需要保证权限声明在<uses-permission>标签中:

<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

CAMERA权限是必须的,没有它所有摄像头相关 API 都会抛SecurityException。WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE是给拍照后保存 JPG 用的。注意 Android 6.0 以后,WRITE 权限被归类为危险权限,但仍然要走动态请求。READ权限在部分 10.0 设备上用于相册获取,所以这里我也加上了。

然后是代码层的动态请求。我一般会在OnCreate或FormShow里预检权限。下面的代码是在 Delphi 10.2 下可用的标准写法,核心是调用TPermissionsService.Current.RequestPermissions,并在回调里判断GrantedResults数组。

uses System.Permissions, System.SysUtils; procedure TMainForm.RequestCameraPermission; var Permissions: TArray<string>; begin Permissions := [ 'android.permission.CAMERA', 'android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.READ_EXTERNAL_STORAGE' ]; TPermissionsService.Current.RequestPermissions( Permissions, procedure(const APermissions: TArray<string>; const AGrantResults: TArray<TPermissionStatus>) var i: Integer; AllGranted: Boolean; begin AllGranted := True; for i := 0 to High(AGrantResults) do begin if AGrantResults[i] <> TPermissionStatus.Granted then AllGranted := False; end; if AllGranted then ActiveCamera else ShowMessage('未获得摄像头权限,无法使用拍照功能'); end); end;

这段代码有个关键点:TPermissionStatus枚举有三个值,Granted、Denied、Unknown。Unknown是用户第一次弹窗时选择“不再询问”之前的状态,如果你只判断Granted而把Unknown当成拒绝,就会在用户还没选择的时候误报。所以我的习惯是把Unknown也视为可以继续尝试的状态,除非明确Denied才提示。但这份资源里的简化写法是全部要求Granted,因为摄像头场景下确实不能冒着未知状态去打摄像头,否则会自动弹系统窗口导致 Activity 焦点问题。

另外要注意:权限请求回调里不能直接操作摄像头,因为弹窗返回后会先进入回调,再恢复 Activity 生命周期。如果你在回调里直接调用Active := True,有可能因为当前窗体还没恢复OnResume而被系统拒绝。正确做法是设置一个全局布尔标志PermissionReady := True,然后在OnResume里再真正打开摄像头。这算是我踩过的一个比较深的坑,后面避坑章节还会提到。

3. 从零跑通摄像头调用:Demo 的目录结构、关键单元与第一个预览画面

拿到这份资源,先不要急着整个工程编译。先花五分钟把目录结构看明白,因为里面除了主界面代码,还有专门处理权限、专门处理图片旋转的工具单元。我先带你过一遍文件职责,再动手跑预览和拍照。

3.1 资源结构解码:拿到包里先看哪几个文件

资源包的核心文件不多,但每个都是可以直接复用的。我拆出来后整理为下面的清单:

文件名类型作用
CameraDemo.dpr工程文件启动入口,包含 Android 权限模块加载
MainForm.pas / .fmx主窗体负责布局TCaptureCameraView、按钮事件
CameraHelper.pas工具单元封装了权限预检、照片旋转、JPG 保存
AndroidManifest.template.xml清单声明权限与 activity 配置
FileProvider 相关配置文件用于 Android 7.0+ 相册获取

先说CameraHelper.pas,这个单元是你最先要读的。它里面不是一个类,而是一组function和procedure。比如有一个函数叫CheckAndRequestPermissions,它内部把System.Permissions的请求过程包了一层,外面调用时只需要传一个回调进去。还有FixImageRotation,它会在拍照回调后读取 JPG 的 EXIF 信息,再做旋转。为什么要单独做这个?后面我会详细说。

MainForm.fmx上的布局很简略:一个TCaptureCameraView放在窗体上方,下方两个按钮,分别是“预览”和“拍照”。这种布局适合直接在真机上测试,但企业级项目里,你通常会把它封装成一个 Frame 嵌到自己的业务页面里。这个 Demo 没有做 Frame 封装,原因是为了降低初学者的理解成本。

3.2 拉起摄像头预览的最小代码块

预览的关键是把TCaptureCameraView和TCameraComponent关联起来,我的习惯是先在FormShow里请求权限,权限通过后再触发activeCamera。下面这段代码是可运行的最小预览逻辑:

procedure TMainForm.activeCamera; begin CameraComponent1.FocusMode := TFocusMode.ContinuousAutoFocus; CameraComponent1.TorchMode := TTorchMode.TorchOff; CameraComponent1.Quality := TQuality.Medium; CaptureCameraView1.CameraComponent := CameraComponent1; CameraComponent1.Active := True; end;

FocusMode设置为ContinuousAutoFocus可以让摄像头在扫码场景下不用手动点按对焦,大部分手机都能在移动时持续自动对焦,但低端机在这个模式下偶尔会频繁对焦导致画面闪烁,所以如果发现画面忽明忽暗,可以改成Manual配合点击对焦事件。

TorchMode是闪光灯控制,这里默认关掉,因为测试预览时开着闪光灯会影响颜色。Quality设置为Medium是一个折中:High画质会占用更多内存,部分 1G 内存的旧设备在预览时直接卡死;Low画质在放大看细节时不如Medium。实际业务里我会根据设备分辨率写一个自适应逻辑,比如屏幕宽度大于 1080 就用 High,否则用 Medium。Quality底层对应的是 Camera.Parameters 里的PictureSize和PreviewSize,但 Delphi 没有把这两个参数分开暴露,所以你只能在三档里做粗粒度控制。

CaptureCameraView1.CameraComponent := CameraComponent1这一步很容易被漏掉。很多新手只设置了CameraComponent1.Active := True,但没把组件绑定到视图上,结果就是摄像头打开了、也占用了资源,但屏幕上什么都没有。这是预览黑屏的最常见原因之一。

这段代码最后一行CameraComponent1.Active := True看起来只是赋个布尔值,实际上它会调用底层Camera.open()并开始预览。在 Delphi 10.2 里,这个属性是异步的:Active置为 True 后,如果打开失败会通过FCameraComponent.Error事件回调,而不是像桌面端那样立即抛异常。所以你要在OnError事件里写日志,否则失败时你根本不知道摄像头被哪个进程占用了。

3.3 拍照与保存:BMP 转 JPG 的两个隐藏参数

拍照回调能拿到一个TBitmap对象,这是 Delphi 封装过后的东家格式。但大多数后端和展示场景都需要 JPG,这时候转换时有两个参数会直接影响最终文件质量:CompressionQuality和ProgressiveEncoding。完整的保存代码如下:

uses System.Types, System.IOUtils, System.SysUtils, FMX.Graphics; procedure TMainForm.CameraComponent1TakePhotoEvent(Image: TBitmap); var JPG: TJPEGImage; FileName: string; begin JPG := TJPEGImage.Create; try JPG.CompressionQuality := 85; JPG.ProgressiveEncoding := False; JPG.Assign(Image); FileName := TPath.GetDocumentsPath + PathDelim + FormatDateTime('yyyy-mm-dd_hhnnss', Now) + '.jpg'; JPG.SaveToFile(FileName); finally JPG.Free; end; end;

CompressionQuality设为 85 是我在“文件体积”和“肉眼质量”之间测出来的平衡点。设成 100 会让一张 1200 万像素的照片动辄接近 2MB,传到后端很亏;设成 70 或者更低,虽然体积小了,但树叶边缘会出现明显锯齿。如果图片最终是打印用途,可以直接用 95,但移动端展示场景 85 足够。

ProgressiveEncoding这个参数建议设成False。渐进式 JPEG 能够在网络上下载时逐渐显示模糊骨架到清晰图像,体验好,但缺点是部分老版本相册应用不兼容,解析时会直接解码失败。我这里为了兼容性,把它关掉了。如果你做的是自研 App,后端也是自己解析,可以设成True试试。

还有一个细节:TPath.GetDocumentsPath在 Android 上对应的是/data/data/包名/files目录,这个目录不需要存储权限,普通用户无法通过文件管理器直接看到。如果你希望图片出现在系统相册里,需要在保存后调一次媒体扫描。常见做法是用 JNI 调用MediaScannerConnection.scanFile,但这又是额外代码。这份资源默认只保存到内部目录,简化了权限逻辑。

4. 把拍照流转成视频流:Surface 与 JNI 回调用法(进阶)

如果你的项目要的不只是拍一张照片,而是要拿到预览帧做实时的图像分析,比如扫码、边缘检测、或者视频流上送,那TCameraComponent的TakePhoto就不够用了。这个时候需要往下走一层,用 JNI 直接操作 Android 原生 Camera。

4.1 Surface 预览与 Camera 类的绑定

Android 原生 Camera 的预览流程分三步:open摄像头、setPreviewDisplay或者setPreviewTexture绑定 Surface、startPreview开始预览。在 Delphi 里,要通过 JNI 把 Surface 对象从 FMX 窗口传过去,这里涉及一个关键对象:JVoid指针和SurfaceTexture。

一个常见做法是把 FMX 的TCaptureCameraView换成你自己创建的TAndroidView,然后在 JNI 层拿到它的 Surface 引用。但实际项目中,我更推荐用一个单独的SurfaceView嵌入,只在 Delphi 侧保留一个预览区域。JNI 绑定的代码如下:

type JCamera = interface(JObject) ['{A1B2C3D4-...}'] function open(cameraId: Integer): JCamera; cdecl; procedure setPreviewDisplay(surface: JSurface); cdecl; procedure startPreview; cdecl; procedure stopPreview; cdecl; procedure release; cdecl; procedure setPreviewCallback(callback: JCamera_PreviewCallback); cdecl; end;

这里的JCamera接口是用 Java 的android.hardware.Camera类做镜像映射。cdecl关键字表示调用约定是 Android 原生 JNI 使用的 C 风格,千万别漏。open方法可以传摄像头 ID,0 是后置,1 是前置。在 Delphi 10.2 里,JStringToString这些类型转换函数在Androidapi.JNI.JavaTypes中都有,但要注意 Camera 类的构造函数不是公开的,不能NewObject,只能调用静态方法open。

绑定 Surface 的代码里有一个很隐蔽的坑:setPreviewDisplay传入的 Surface 必须来自一个已经初始化的 SurfaceView,而且这个 SurfaceView 必须已经添加到窗体并且完成布局。如果你在OnCreate阶段就调用setPreviewDisplay,会得到一个NullPointerException,因为此时 Surface 还没有创建完成。正确做法是监听SurfaceView的SurfaceHolder.Callback,在surfaceCreated事件后再把 Surface 传给 Camera。

4.2 JNI 回调代码怎么写才不崩:三处必须注意

第一个必须注意的地方是线程切换。setPreviewCallback注册的回调用onPreviewFrame(byte[] data, Camera camera),这个回调运行在系统 Binder 线程,不是主线程。在这个回调里直接操作 FMX 的TLabel、TImage会引发CalledFromWrongThreadException。解决方法是把数据拷贝出来后用TThread.Queue放到主线程:

procedure TMyCamera.PreviewCallback(data: TJavaArrayByte); begin TThread.Queue(nil, procedure begin // 更新主线程上的 Image 控件 FImage.Bitmap.Assign(data); end); end;

注意data参数是TJavaArrayByte,它是一个 Java 数组对象。你在回调里必须先在子线程把它复制成一个 Delphi 的TBytes,因为TJavaArrayByte的生命周期由 JNI 管理,回调结束就会被释放。等TThread.Queue里的任务真正执行时,data早就不可用了。这是个非常容易踩的坑,我用两个星期的血泪经验来背书。

第二个必须注意的是Camera实例的生命周期。在一个Form里,如果你在OnClose事件中没有调用stopPreview和release,那么摄像头就会被一直占用。下一次再进入页面打开摄像头时,底层会返回Camera is being used by another process。这种现象在真机上表现为第一次打开正常,第二次以后全部黑屏,然后过几秒闪退。解决方法是在FormDestroy里强制释放:

procedure TMainForm.FormDestroy(Sender: TObject); begin if FNativeCamera <> nil then begin FNativeCamera.stopPreview; FNativeCamera.release; FNativeCamera := nil; end; end;

第三个注意点是预览回调的数据格式。onPreviewFrame返回的data默认是 YUV420SP 格式,也就是 NV21。这种格式在 Delphi 里没有现成的显示控件,你要把它转成 RGB 再生成 Bitmap。转换算法是固定的,但要注意行列字节对齐:有些设备会在每行末尾补齐几个像素,导致直接按 width 计算索引时图像错位。所以从 JNI 回调拿数据后,不要假设data.Length = width * height * 3 / 2,应该用Camera.Parameters的PreviewSize去动态计算。这也是很多开源库扫码不准的根源之一。

5. 常见问题排查与避坑:从黑屏到闪退的 6 个真实案例

这一章我整理了在实际测试中反复出现的六个问题。每一条都按“现象-原因-解决”的方式写,你可以直接照着排查。

5.1 黑屏但权限正常,预览区一片黑

现象:按钮能点,权限也弹了,CameraComponent.Active设置为True后没报错,但TCaptureCameraView区域始终黑着。

原因:CaptureCameraView.CameraComponent属性没有在Active := True之前赋值,或者赋值的组件根本不是同一个对象。Delphi 的TCaptureCameraView是独立控件,它内部持有对 CameraComponent 的引用,但如果你在FormDesigner里拖的是另一个TCameraComponent,运行时就可能配不上。还有一个原因是摄像头权限虽然已经允许,但你在权请求回调里设置了Active := True,当时 Activity 还在 Paused 状态,open成功但预览 Surface 没有初始化。

解决:严格按顺序执行,先绑定CaptureCameraView.CameraComponent := CameraComponent1,再等FormShow的OnResume消息里设置Active。同时给CameraComponent1.OnError写一个断点,把错误信息打出来。

5.2 拍照后照片旋转 90 度

现象:拍出来的 JPG 用电脑打开正常,但上传到服务器后系统相册里预览是横的。查看 JPG 元数据发现 Orientation 值为 6。

原因:Android 摄像头传感器默认方向和屏幕方向不一致。大部分手机屏幕是竖屏,但相机传感器是横屏。系统相册应用会根据 JPG 的 EXIF Orientation 字段自动旋转,但很多 Delphi 保存 JPG 的代码不会写入这个字段。Delphi 的TJPEGImage.Assign(Image)不会保留 EXIF 信息,所以照片丢了旋转标记。

解决:我在CameraHelper.pas中写了FixImageRotation函数,核心是读取 JPG 文件头里的 Orientation 值,如果值大于 1,就用TBitmap.Rotate把它转正,再重新保存。需要注意:这个函数必须在保存 JPG 之前调用,而不是之后。实际操作中,用TJpegImage读原始文件,再写回,这样会丢失原图的所有 EXIF 信息,包括拍摄时间、地理位置,所以如果你需要保真,建议在保存时自己构造一个 Exif 写入。但作为通用工具,直接旋转成正向图片最省事。

5.3 打开摄像头后 Apply 崩溃

现象:在部分 Android 8.0 低端机上,点击“拍照”按钮,程序直接闪退,Logcat 里有JNI ERROR (app bug): local reference table overflow。

原因:这是因为拍照回调中读取的TBitmap被多次复制,而每次复制只创建了JLocalRef但没有释放。Delphi 的 JNI 桥接层对于多个TJavaArray会累积 local reference,超过 16KB 就会溢出。

解决:拍照回调里避免做复杂处理,不要多次访问Image.Bits。最好的做法是一次性把TBitmap转成 JPG 写入文件,然后立刻释放。另外,可以在Androidapi.JNI.GraphicsContentViewText里手动调用DeleteLocalRef,但那样代码比较繁琐,不如保持回调轻量。我一般会把拍照结果放在一个成员变量里,由主线程定时器去处理,而回调里只做FResultBitmap.Assign(Image)。

5.4 相册选择图片时,拍完的图片无法立即显示

现象:拍照保存后,打开相册看不到刚刚那张照片,要等很久或重启手机才出现。

原因:因为没有触发系统媒体扫描。文件写入内部目录后,系统相册数据库不知道该文件存在。很多开发者误以为保存到DCIM文件夹就没事,实际上 Android 的 MediaProvider 是异步扫描的,尤其是新文件如果命名不对或目录不对,扫不到。

解决:保存到TPath.GetSharedCameraPath或者Environment.DCIM目录,然后调一次媒体扫描。在 Delphi 中调用MediaScanner可以这样写:

procedure ScanMedia(AFilePath: string); var JFilePath: JString; JUri: JUri; begin JFilePath := StringToJString(AFilePath); JUri := TJMediaScannerConnection.JavaClass.scanFile(JFilePath); end;

这个方法在 Android 7.0 以后需要 FileProvider,否则传file://路径会抛FileUriExposedException。这也是我为什么在资源包里单独放了 FileProvider 配置。

5.5 首次弹窗点“拒绝”后,再次打开页面不再弹窗

现象:第一次权限请求用户选择了“拒绝”,之后每次进入页面都没有权限请求弹窗,摄像头预览一直黑屏。

原因:Android 6.0+ 的权限机制规定,如果用户连续拒绝两次,或者第一次拒绝时勾选了“不再询问”,系统不会再弹窗,只会返回Denied。如果你没有做特殊处理,就会卡死。

解决:在权限回调里检测TPermissionStatus.Denied,然后跳转到系统设置页,引导用户手动打开。代码示例:

if AStatus = TPermissionStatus.Denied then TAndroidHelper.Activity.getIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); Intent := TJIntent.Create; Intent.setAction(TJSettings.JavaClass.ACTION_APPLICATION_DETAILS_SETTINGS); ...

注意ACTION_APPLICATION_DETAILS_SETTINGS的 URI 需要带包名,类似package:com.xxx.xxx。跳转之后用户从设置页回来,要在OnResume里重新检查权限,而不是直接调用摄像头。

5.6 JNI 回调更新 UI 导致闪退

现象:使用 JNI 直调预览回调,每次进来都会随机闪退,Logcat 提示Only the original thread that created a view hierarchy can touch its views。

原因:这个原因前面已经说过,回调在 Binder 线程,而 FMX 的控件只能在主线程操作。这几乎是所有从 Android 回调到 Delphi 的通用问题。

解决:规则很简单,所有 UI 更新必须通过TThread.Synchronize或TThread.Queue。但要注意这个回调频率很高,每帧都触发,如果每秒 30 帧,每帧都Queue一个任务,主线程会被淹没。我的习惯是做一个采样控制:只在画面变化大于阈值时才Queue;如果只是显示预览,就直接用 Canvas 异步下画。资源里提供了一个TFrameProcessor类,专门做这个降频逻辑。

6. 一个能提升成功率的小习惯:先写好权限降级与复用逻辑

在拆完这份资源的代码之后,我最想分享的一个习惯不是某个 API 的用法,而是一个权限请求的降级策略。你可能会觉得权限请求就是弹窗点一下,其实它才是摄像头调用的最大不确定性来源。用户可能点拒绝,可能选不再询问,可能在系统设置里关了又开,这些状态组合起来足以让一套稳定的拍照功能变成玄学。

我的做法是写一个独立的权限管理单元,叫PermissionGate.pas,它对外暴露唯一的函数CheckOrRequestPermission。这个函数会先查询当前权限状态,如果已经是Granted,直接触发回调;如果不是,才弹权限请求。在回调中,如果收到Denied,我不会直接提示“无法使用”,而是弹一个自定义对话框,解释为什么需要这个权限,然后给两个按钮:“去设置”和“重试”。“去设置”会跳转系统应用详情页,“重试”会重新触发RequestPermissions。只有当用户连续两次拒绝,才真正关闭功能入口。

这样做的好处是把所有权限分支都收敛到一个地方,业务代码里不再散落if PermissionStatus = Denied之类的判断。这样做也方便做自动化测试:我可以模拟用户拒绝一次、拒绝两次、允许等不同路径,逐一验证回调不会崩。自从我把这套逻辑加进所有摄像头相关项目,真机联调时间至少缩了一半。

另一个小技巧是把权限预检和视频预览的打开做一个“节流”。也就是说,不在每次FormShow都重复请求权限和打开摄像头,而是先判断摄像头组件的Active状态。如果上一个页面已经打开了摄像头,下一次进入时先释放,再重新绑定预览视图。这个顺序如果反了,必现黑屏。从那以后我每次做 Delphi 摄像头功能都强制走一遍权限预检和释放流程,再没遇到因为权限顺序导致的黑屏闪退。希望这份资源的拆解对你有用,照着跑通预览、拍照和权限处理,你就已经跨过了 Delphi Android 摄像头开发最大的那道坎。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询