大家好我是[晚风依旧似温柔]新人一枚欢迎大家关注~本文目录一、CameraX 思路我们要的是“用例驱动”的相机而不是一坨状态机二、相机权限不把权限搞清楚你连第一帧预览都看不到2.1 需要哪些权限2.2 在 module.json5 中静态声明2.3 动态申请运行时让用户点“允许”三、预览 拍照 录像把相机能力拆成几条清晰的链路3.1 初始化 Camera 管理器 选择设备3.2 绑定预览 SurfaceXComponent3.3 拍照触发 PhotoOutput 的 capture3.4 录像VideoOutput 媒体录制四、相册写入拍完不入库用户就永远找不到4.1 使用 MediaLibrary 写入图片 / 视频五、自定义相机 UI 实战自己做一个“像样的相机页面”5.1 UI 结构5.2 可扩展的交互细节六、常见坑 心理安慰小合集 最后一点小结 一、CameraX 思路我们要的是“用例驱动”的相机而不是一坨状态机先说清楚一点CameraX 是 Android 上的一个高级相机库它的设计思路非常值得借鉴以 UseCase 为中心Preview / ImageCapture / VideoCapture自动帮你处理生命周期、线程、Camera 选择等复杂细节使用方式非常简洁在鸿蒙上我们完全可以模仿 CameraX 的思想→ “我不想每次都直接跟底层 camera session 互怼→ 我只想写我要预览 / 我要拍照 / 我要录像。”所以我们可以做一层自己的“类 CameraX 封装”CameraController管理 Camera 设备、生命周期、状态负责打开 / 关闭 / 切换前后摄像头PreviewUseCase负责预览画面绑定到 UIXComponent / Surface 等PhotoUseCase负责拍照、输出图片数据VideoUseCase负责开启 / 停止录像并输出文件路径你没必要真的叫这些名字但“按职责拆分”这个思路一定要有不然代码会拧成一团。二、相机权限不把权限搞清楚你连第一帧预览都看不到说实话相机相关问题有相当一部分是权限没配对。我们先把权限这块讲清楚再谈相机本身。2.1 需要哪些权限典型拍照 / 录像 写入相册场景基本要这些ohos.permission.CAMERA—— 相机采集ohos.permission.MICROPHONE—— 录像录音ohos.permission.READ_MEDIA—— 读取媒体有时用于相册读取ohos.permission.WRITE_MEDIA—— 写入媒体保存照片 / 视频有的版本会把读写合并为媒体访问相关权限整体方向一样相机 麦克风 媒体存储这三件事。2.2 在 module.json5 中静态声明{ module: { // ... requestPermissions: [ { name: ohos.permission.CAMERA }, { name: ohos.permission.MICROPHONE }, { name: ohos.permission.READ_MEDIA }, { name: ohos.permission.WRITE_MEDIA } ] } }这一步是“跟系统打招呼”没有声明很多相机 / 媒体 API 直接不给用。2.3 动态申请运行时让用户点“允许”相机 麦克风一般属于受控权限需要运行时弹窗申请importabilityAccessCtrlfromohos.abilityAccessCtrl;asyncfunctionensurePermission(context,permission:string):Promiseboolean{constatManagerabilityAccessCtrl.createAtManager();conststatusawaitatManager.checkAccessToken(context.tokenId,permission);if(statusabilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){returntrue;}constresultawaitatManager.requestPermissionsFromUser(context,[permission]);returnresult.authResults[0]abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;}exportasyncfunctionensureCameraPermissions(context):Promiseboolean{constperms[ohos.permission.CAMERA,ohos.permission.MICROPHONE,ohos.permission.READ_MEDIA,ohos.permission.WRITE_MEDIA];for(constpofperms){constokawaitensurePermission(context,p);if(!ok){console.error(【权限被拒绝】${p});returnfalse;}}returntrue;} 建议不要一进 App 就弹在用户点击“拍照/录像”入口的时候再申请顺便配一个简单说明“为了拍照 / 录像需要相机和麦克风权限”。三、预览 拍照 录像把相机能力拆成几条清晰的链路大部分相机功能可以抽象成三条链预览Camera → Surface → UI拍照Camera → Image → 保存文件录像Camera → VideoStream → 封装为媒体文件以鸿蒙多媒体 camera 能力为例大致流程如下伪代码思路3.1 初始化 Camera 管理器 选择设备importcamerafromohos.multimedia.camera;classCameraController{privatecameraManager:camera.CameraManager|nullnull;privatecameraDevice:camera.CameraDevice|nullnull;privatesession:camera.Session|nullnull;privatepreviewOutput:camera.PreviewOutput|nullnull;privatephotoOutput:camera.PhotoOutput|nullnull;privatevideoOutput:camera.VideoOutput|nullnull;constructor(privatecontext){}asyncinit(){this.cameraManagercamera.getCameraManager(this.context);constcamerasthis.cameraManager.getSupportedCameras();// 简单选一个后置摄像头this.cameraDevicecameras.find(cc.positioncamera.CameraPosition.REAR)??cameras[0];}}实战建议把“选择前/后摄像头”封装成单独方法switchCamera()对“没有前摄像头”这种情况给出兼容处理3.2 绑定预览 SurfaceXComponent在 ArkUI 里面要把相机画面渲染出来通常会用一个XComponent作为渲染目标Surface。UI 侧importcamerafromohos.multimedia.camera;EntryComponentstruct CameraPage{privatecontroller:CameraControllernewCameraController(getContext(this));privatesurfaceId:string;asyncaboutToAppear(){constokawaitensureCameraPermissions(getContext(this));if(!ok){// TODO: 打提示return;}awaitthis.controller.init();}onPreviewReady(surfaceId:string){this.surfaceIdsurfaceId;this.controller.startPreview(surfaceId);}build(){Column(){XComponent({id:cameraPreview,type:surface,controller:newXComponentController()}).onLoad((component){constsurfaceIdcomponent.getSurfaceId();this.onPreviewReady(surfaceId);}).width(100%).height(70%)// 下方按钮Row(){Button(拍照).onClick(()this.controller.takePhoto());Button(开始录像).onClick(()this.controller.startRecord());Button(停止录像).onClick(()this.controller.stopRecord());}.justifyContent(FlexAlign.Center)}}}控制器侧asyncstartPreview(surfaceId:string){if(!this.cameraManager||!this.cameraDevice)return;constpreviewProfilethis.cameraManager.getSupportedOutputCapability(this.cameraDevice).previewProfiles[0];constsurfacecamera.createSurfaceFromId(surfaceId);// 示意this.previewOutputthis.cameraManager.createPreviewOutput(previewProfile,surface);this.sessionthis.cameraManager.createSession();this.session.beginConfig();this.session.addOutput(this.previewOutput);this.session.commitConfig();this.session.start();}这块具体 API 根据版本略有差异但思路都是拿到 Camera → 选 profile → 创建 Output预览→ 建 session → 绑定 surface → start()3.3 拍照触发 PhotoOutput 的 capture接下来看拍照链路。配置PhotoOutput调用capture()拿到图片数据 → 落盘 → 写入相册简化示例控制器内部asyncpreparePhotoOutput(){constcapthis.cameraManager.getSupportedOutputCapability(this.cameraDevice);constphotoProfilecap.photoProfiles[0];// 创建一个 photoOutput一般基于某种 image 或 bufferthis.photoOutputthis.cameraManager.createPhotoOutput(photoProfile);this.photoOutput.on(photoAvailable,async(photo:camera.Photo){// 拿到图片数据constbufferphoto.main;// ArrayBufferawaitthis.savePhotoToGallery(buffer);});this.session.beginConfig();this.session.addOutput(this.photoOutput);this.session.commitConfig();}asynctakePhoto(){if(!this.photoOutput){awaitthis.preparePhotoOutput();}this.photoOutput.capture();}这里我们先不纠结具体类型重点放在“拍照 → 回调拿数据 → 保存”这一整条链。3.4 录像VideoOutput 媒体录制录像比拍照稍微复杂一点因为要持续向一个媒体文件写入视频流。一般流程是配置VideoOutput指定编码参数创建Recorder或类似对象session 中加入 videoOutputstartRecord() / stop()简化版伪代码思路asyncprepareVideoOutput(filePath:string){constcapthis.cameraManager.getSupportedOutputCapability(this.cameraDevice);constvideoProfilecap.videoProfiles[0];this.videoOutputthis.cameraManager.createVideoOutput(videoProfile);// 将 videoOutput 与 recorder 关联不同版本写法不同this.session.beginConfig();this.session.addOutput(this.videoOutput);this.session.commitConfig();// 同时初始化 recorder 写入 filePath}asyncstartRecord(){// 启动相机 session若未启动this.session.start();// 启动 recorder 或 videoOutput// this.videoOutput.start(); 等}asyncstopRecord(){// 停止 recorder// this.videoOutput.stop();// 停止 session 或保持 preview}真实项目中这块要对照 SDK 示例但结构是一样的相机出视频流 → 媒体录制器按编码配置写成文件。四、相册写入拍完不入库用户就永远找不到很多人拍照写完文件就结束了结果用户在相册找不到刚刚拍的照片以为你没保存。要让媒体真正出现在系统图库里得走一把媒体库MediaLibrary。4.1 使用 MediaLibrary 写入图片 / 视频典型写入流程示意importmediaLibraryfromohos.multimedia.mediaLibrary;asyncsavePhotoToGallery(buffer:ArrayBuffer):Promisestring{constcontextthis.context;constmlmediaLibrary.getMediaLibrary(context);constfileNameIMG_${Date.now()}.jpg;constmediaTypemediaLibrary.MediaType.IMAGE;constassetawaitml.createAsset(mediaType,fileName,Pictures/Camera);constfdawaitasset.open(rw);// 写数据constfileIoawaitfs.open(fd);// 伪代码视 fs 模块具体用法awaitfileIo.write(buffer);awaitfileIo.close();// 通知媒体库刷新awaitml.close();returnasset.uri;// 或者返回 path}视频类似只是媒体类型改成VIDEO目录改成Movies/Camera之类。确保目录使用系统建议的相册路径例如 Camera写完后关闭文件描述符必要时触发媒体扫描一般 MediaLibrary API 会处理五、自定义相机 UI 实战自己做一个“像样的相机页面”看了这么多流程如果 UI 还停留在“一个 XComponent 三个 Button”的 Demo 水平那就太可惜了。我们来设计一个更像产品的页面上方相机预览中间辅助信息层比如对焦框、提示文字底部左缩略图最近一张照片中拍照 / 录像按钮右切换前后摄像头5.1 UI 结构EntryComponentstruct CustomCameraPage{privatecameraCtrl:CameraControllernewCameraController(getContext(this));privatexController:XComponentControllernewXComponentController();StateisRecording:booleanfalse;StateusingFront:booleanfalse;asyncaboutToAppear(){constokawaitensureCameraPermissions(getContext(this));if(!ok)return;awaitthis.cameraCtrl.init();}onPreviewReady(){constsurfaceIdthis.xController.getSurfaceId();this.cameraCtrl.startPreview(surfaceId);}build(){Column(){// 预览区域XComponent({id:cameraPreview,type:surface,controller:this.xController}).onLoad(()this.onPreviewReady()).width(100%).height(70%)// 可叠加一些 overlay比如对焦框、提示等这里先略// 底部控制条Row(){// 左侧缩略图位占位Circle().width(40).height(40).backgroundColor(0x333333)// 中间大按钮Circle().width(70).height(70).backgroundColor(this.isRecording?0xff0000:0xffffff).margin({left:50,right:50}).onClick(async(){if(this.isRecording){awaitthis.cameraCtrl.stopRecord();this.isRecordingfalse;}else{// 拍照模式 / 录像模式可根据需求切换awaitthis.cameraCtrl.takePhoto();}})// 右侧切换摄像头Button(this.usingFront?前置:后置).onClick(async(){this.usingFront!this.usingFront;awaitthis.cameraCtrl.switchCamera(this.usingFront);constsurfaceIdthis.xController.getSurfaceId();this.cameraCtrl.startPreview(surfaceId);})}.height(30%).justifyContent(FlexAlign.Center)}.backgroundColor(0x000000).width(100%).height(100%)}}这个 UI 示例的重点是告诉你预览与控制可以完全按产品要求排版相机只是一块“画面源”UI 完全由你决定拍照、录像、切换摄像头、显示缩略图都可以组合5.2 可扩展的交互细节你可以继续往里加很多“产品级体验”点击预览区域对焦 / 显示对焦动画滑动调整缩放Pinch 手势 → 调整 Zoom长按拍照按钮 → 切换成连拍模式左滑打开相册录像时显示录制时长 / 剩余空间提示在代码层面都可以围绕CameraController增加对应接口比如setZoom(level:number){}setFlashMode(mode:auto|on|off){}lockFocusAt(x:number,y:number){}UI 这块完全可以做得很“产品化”不要满足于 SDK Demo 级别。六、常见坑 心理安慰小合集 最后简单列几个你几乎一定会遇到的坑至少我都挨过预览是横的UI 是竖的记得处理旋转 / 映射或者使用 SDK 提供的 orientation 控制照片方向颠倒 / 旋转 90°EXIF 信息 / orientation 没处理录像没声音MIC 权限 / 音频编码配置缺失拍完照片在相册里找不到媒体库没正确写入 / 目录不对 / 没触发扫描后台切换回来相机崩 / 黑屏生命周期没处理onBackground 里该停的停、该释放的释放回到前台时重新创建 session preview写相机真的很少“一把过”但你把本文这些流程和结构吃透之后再遇到 bug至少心里有根线——问题一定在 权限 / 会话 / 输出配置 / 媒体库 这几块。最后一点小结 如果你看完这篇还能在脑子里清楚画出这条线相机权限 ✅ → 初始化 CameraManager Device ✅ → 建 Session PreviewOutput ✅→ 自定义 UI 绑定 Surface ✅ → 拍照链路PhotoOutput 保存到相册 ✅→ 录像链路VideoOutput 录制文件 ✅ → 媒体库写入 相册可见 ✅那你已经告别“只能跑 Demo”的阶段了开始摸到“可上线产品级相机”的门槛了。如果觉得有帮助别忘了点个赞关注支持一下~喜欢记得关注别让好内容被埋没