当前位置: 首页 > news >正文

第25篇|Surface 预览控制:ArkUI 页面如何接住相机画面

相机预览不是把 CameraKit 打开就能显示出来。ArkUI 页面需要先提供 XComponent 的 surfaceId,CameraKit 才能创建 PreviewOutput,把预览帧写进页面。

学习目标

  • 理解 XComponent、surfaceId、PreviewOutput 的关系。
  • 能解释为什么 Surface 创建后要延迟准备相机能力。
  • 掌握 Surface 销毁时释放预览资源的原因。
  • 知道 frameStart 为什么是判断预览真正连上的关键事件。

一、预览画面来自哪条链路

很多人做相机页时会把注意力放在 CameraManager 和 PhotoSession 上,但真正在屏幕上显示画面的入口是 Surface。ArkUI 侧 XComponent 创建出 surfaceId,项目把这个 id 传给 CameraKit 的 createPreviewOutput,CameraKit 后续输出的预览帧才有落点。

所以第 25 篇关注的不是“拍照”,而是“页面如何接住预览”。只要 Surface 生命周期没处理好,后面的权限和会话都可能是正确的,用户看到的仍然是黑屏。

图 1 相机页 Surface 到 PreviewOutput 的运行链路

二、PreviewSurfaceController:只做生命周期转发

项目里没有把 Surface 创建后的业务逻辑直接塞进 XComponent,而是做了一个 PreviewSurfaceController。它继承 XComponentController,内部只保存 createHandler 和 destroyHandler。当 onSurfaceCreated 到来时,把 surfaceId 转交给页面状态;当 onSurfaceDestroyed 到来时,通知页面清理资源。

这个设计很小,但很重要:控制器不关心相机权限、不创建 CameraInput,也不操作 PhotoSession。它只负责把 ArkUI 生命周期转换成页面能处理的事件。这样后续如果要换预览组件或抽出通用控制器,也不会牵动 CameraKit 主流程。

图 2 PreviewSurfaceController 只转发 Surface 创建和销毁事件

class PreviewSurfaceController extends XComponentController { private createHandler?: (surfaceId: string) => void; private destroyHandler?: (surfaceId: string) => void; setCreateHandler(handler: (surfaceId: string) => void): void { this.createHandler = handler; } setDestroyHandler(handler: (surfaceId: string) => void): void { this.destroyHandler = handler; } onSurfaceCreated(surfaceId: string): void { this.createHandler?.(surfaceId); } onSurfaceDestroyed(surfaceId: string): void { this.destroyHandler?.(surfaceId); } }

三、Surface 创建后为什么要 schedule,而不是马上 open

aboutToAppear 中,项目给 backSurfaceController 和 frontSurfaceController 分别设置回调。后摄 Surface 创建后会保存 backSurfaceId,并调用 scheduleCameraCapabilityPrepare(80)。这里不是随手加延迟,而是给页面布局、Surface 绑定和权限状态一个稳定窗口。

Surface 销毁时,项目会清空对应 id,并调用 teardownDualPreview。原因也直接:CameraKit 的输出还绑定着旧 surfaceId,如果页面切走、组件重建或多窗口尺寸变化时不释放,下一次创建会话很容易复用到已经失效的输出。

图 3 Surface 创建保存 id,销毁时释放预览资源

this.backSurfaceController.setCreateHandler((surfaceId: string) => { this.backSurfaceId = surfaceId; this.scheduleCameraCapabilityPrepare(80); }); this.backSurfaceController.setDestroyHandler(() => { this.backSurfaceId = ''; void this.teardownDualPreview(!this.shouldPreserveSequentialCaptureContext()); }); this.frontSurfaceController.setCreateHandler((surfaceId: string) => { this.frontSurfaceId = surfaceId; void this.ensureCameraPreview(); }); this.frontSurfaceController.setDestroyHandler(() => { this.frontSurfaceId = ''; void this.teardownDualPreview(!this.shouldPreserveSequentialCaptureContext()); });

四、PreviewOutput:把 profile 和 surfaceId 绑定起来

当单摄预览真正启动时,项目先通过 capability 找到 previewProfiles,再调用 createPreviewOutput,把 preview profile 与 backSurfaceId 组合起来。随后给 PreviewOutput 绑定 frameStart 回调。frameStart 出现,说明预览帧已经开始到达页面,这比“start 方法调用成功”更接近用户真正看到画面的时刻。

这也是 UI 状态更新的依据。创建会话成功但 frameStart 没来时,页面仍可提示“预览连接中”;frameStart 到来后,再把预览状态切到 live。这样用户不会被一个已经 start 但还没有画面的页面误导。

图 4 PreviewOutput 绑定 surfaceId 并监听 frameStart

this.singleCameraInput = this.cameraManager.createCameraInput(this.singleCameraDevice); await this.singleCameraInput.open(); this.singlePreviewOutput = this.cameraManager.createPreviewOutput( capability.previewProfiles[0], this.backSurfaceId ); this.singlePreviewOutput.on('frameStart', () => { this.handleSinglePreviewFrameStart(activeRole); }); this.singlePhotoOutput = this.cameraManager.createPhotoOutput(this.pickBestPhotoProfile(capability.photoProfiles)); this.bindPhotoOutput(activeRole, this.singlePhotoOutput, 'single'); this.singlePhotoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; this.singlePhotoSession.beginConfig(); this.singlePhotoSession.addInput(this.singleCameraInput); this.singlePhotoSession.addOutput(this.singlePreviewOutput); this.singlePhotoSession.addOutput(this.singlePhotoOutput); await this.singlePhotoSession.commitConfig(); await this.singlePhotoSession.start(); this.cameraSessionActive = true; this.syncZoomStateFromSession(); this.refreshCameraFlashState();

五、预览稳定性的三个实操判断

第一,看 surfaceId 是否为空。只要 backSurfaceId 为空,就不要创建 PreviewOutput;应该等待 Surface 创建回调。第二,看 previewProfiles 是否为空。能力集没有预览规格时,不要硬传空数组。第三,看销毁路径是否完整。Surface 销毁后不释放输入、输出和会话,会让下一次进入相机页变成随机失败。

项目把这些判断放在 prepareCameraCapability、ensureSinglePreview 和 teardownDualPreview 三个位置:入口负责等条件,会话负责连接输出,释放负责把旧资源断干净。写相机预览时,能把这三层分清楚,代码就不会到处补 if。

本篇检查清单

  • PreviewSurfaceController 中没有混入 CameraKit 业务逻辑。
  • Surface 创建时保存 surfaceId,并延迟触发能力准备。
  • Surface 销毁时会清空 id,并释放旧预览资源。
  • PreviewOutput 使用真实 surfaceId 创建,并监听 frameStart。
  • 正文配图包含运行链路图、Surface 控制器源码、Surface 回调源码和 PreviewOutput 源码。

今日练习

  • 在 Surface 创建回调中打印 backSurfaceId 长度,确认页面切换时会重新创建。
  • 把 schedule 延迟临时改成 0,对比部分机型预览连接时序是否更容易不稳定。
  • 在 frameStart 回调中记录第一次帧到达时间,作为预览启动耗时指标。
http://www.jsqmd.com/news/917757/

相关文章:

  • APP攻防-资产收集篇反代理反证书反模拟器MsgiskLSP模块系统证书
  • Win10激活失败?可能是你的批处理脚本没做好这3步检查(网络/版本/密钥详解)
  • 用Scratch打造钩针图案生成器:连接编程与手工的创意实践
  • 猫抓Cat-Catch:浏览器视频下载神器,一键嗅探网页媒体资源完整指南
  • 2026年 西安消防器材/消防设备/消防设施/灭火器材/应急消防器材最新推荐:精选品牌与实战性能深度解析! - 品牌企业推荐师(官方)
  • 从假设检验到机器学习:正态分布与卡方分布在数据分析中的实战联动指南
  • WarcraftHelper终极指南:让经典魔兽争霸3焕发新生,解决所有版本兼容问题
  • 解锁小说离线阅读新可能:novel-downloader重新定义数字阅读体验
  • 乔布斯教会耄耋的事:在《一念成仙》,耄耋如何定义“最好的产品”
  • 【Google内部未公开】Gemini部署文档黄金结构模板(含YAML校验规则+CI/CD嵌入点)
  • 如何用SMUDebugTool解锁AMD Ryzen处理器的终极性能:完全指南
  • 别再死记硬背了!用Kettle+MySQL手把手还原一个‘客户忠诚度分级’复杂存储过程
  • COM3D2.MaidFiddler:如何用实时编辑器快速修改COM3D2女仆属性
  • 如何快速批量添加专业水印?semi-utils智能工具让你告别手动烦恼
  • 2026电子版证件照怎么手机做?保姆级方法教程+软件推荐手把手教你 - 软件小管家
  • 横向辅助驾驶及人机共驾控制策略优化【附仿真】
  • 终极指南:使用msoffcrypto-tool轻松解锁加密Office文档
  • 5分钟搞定200+小说网站:novel-downloader离线阅读终极指南
  • 2026青岛家装公司靠谱榜单出炉!综合实力、口碑、性价比实测参考 - 商业新知
  • 5步实现加密音频格式转换:开源工具深度解析与应用指南
  • UniApp + Painter实战:从‘社交裂变’到‘数据报告’,解锁小程序图片生成的3个高级应用场景
  • 告别深夜夺命Call:如何利用 AI Agent Skills 自动自愈生产环境故障
  • HS2-HF Patch终极指南:如何轻松优化你的Honey Select 2游戏体验
  • 基于SCARA机械臂的DIY写字钟:从运动学算法到嵌入式实现
  • 免费数据恢复神器:TestDisk与PhotoRec的终极使用指南
  • 预训练模型破解AI搜索冷启动:从BERT到向量检索的实战指南
  • 基于Arduino与游戏手柄的机器人手臂糖果分发系统设计与实现
  • 告别杜邦线乱飞!用Arduino Uno和TM1650驱动数码管模块,一个IIC接口搞定四位显示
  • 2026年西安管道厂家/消防管道/防腐管道/保温管道/高压管道/埋地管道/市政管道/通风管道/镀锌管/无缝管/排水管道公司实力推荐榜 - 品牌企业推荐师(官方)
  • 2026石家庄手表回收真实成交 全套附件价更高 - 薛定谔的梨花猫