HarmonyOS 6.1 实现一键同时拍摄横竖屏照片

HarmonyOS 6.1 实现一键同时拍摄横竖屏照片 前言最近在做一个相机应用时遇到个需求用户拍一张照片希望同时得到横屏和竖屏两个版本方便在不同场景使用。比如发朋友圈用竖屏发微博用横屏不用再手动裁剪。一开始想的是用两个PhotoOutput同时拍摄但实际测试发现HarmonyOS的相机框架对多PhotoOutput支持有限而且会增加性能开销。后来换了个思路拍一张高分辨率照片然后智能裁剪成横竖两个版本效果反而更好。实现原理整体思路很简单拍摄一张高分辨率照片比如4:3或16:9的原图从原图中心区域裁剪出竖屏版本9:16比例从原图中心区域裁剪出横屏版本16:9比例两张照片都保存到相册关键点在于裁剪算法不是简单粗暴地切而是根据原图尺寸智能计算裁剪区域尽可能保留更多画面内容。为什么不用双PhotoOutput试过用两个PhotoOutput分别设置不同的分辨率配置但遇到几个问题部分设备不支持同时添加多个PhotoOutput即使支持两个输出的分辨率选择受限性能开销大拍摄延迟明显相比之下单张高分辨率照片裁剪的方案兼容性好所有设备都支持可以精确控制裁剪区域性能开销小裁剪速度很快核心代码实现1. 相机初始化首先需要初始化相机会话这里用了两路预览流一路显示在XComponent上另一路通过ImageReceiver处理帧数据用于小窗预览。exportasyncfunctioninitCamera(surfaceId:string):Promisevoid{constmgrcamera.getCameraManager(getContext())constcamerasmgr.getSupportedCameras()constbackCameracameras.find(cc.cameraPositioncamera.CameraPosition.CAMERA_POSITION_BACK)??cameras[0]constcapmgr.getSupportedOutputCapability(backCamera,camera.SceneMode.NORMAL_PHOTO)// 创建双拍摄处理器dualCapturenewDualPhotoCapture()awaitdualCapture.createOutputs(mgr,cap)cameraInputmgr.createCameraInput(backCamera)awaitcameraInput.open()// 主预览流 - 选择16:9的最高分辨率constpreviewProfilecap.previewProfiles.filter(pMath.abs(p.size.width/p.size.height-9/16)0.1).reduce((best,p)p.size.width*p.size.heightbest.size.width*best.size.height?p:best,cap.previewProfiles[0])constpreviewOutputmgr.createPreviewOutput(previewProfile,surfaceId)// 第二路预览流 - 用于小窗显示imageReceiverimage.createImageReceiver(previewProfile.size,image.ImageFormat.YUV_420_888,2)constreceiverSurfaceIdawaitimageReceiver.getReceivingSurfaceId()constsecondPreviewOutputmgr.createPreviewOutput(previewProfile,receiverSurfaceId)// 启动帧处理循环_startFrameLoop(imageReceiver,previewProfile.size.width,previewProfile.size.height)// 配置会话photoSessionmgr.createSession(camera.SceneMode.NORMAL_PHOTO)ascamera.PhotoSession photoSession.beginConfig()photoSession.addInput(cameraInput)photoSession.addOutput(previewOutput)photoSession.addOutput(secondPreviewOutput)dualCapture.addOutputsToSession(photoSession)awaitphotoSession.commitConfig()awaitphotoSession.start()sessionReadytrue}这里有几个细节预览流选择优先选16:9比例的最高分辨率这样预览画面更接近最终照片双预览流第一路直接显示第二路通过ImageReceiver处理用于实时生成小窗预览会话配置按照beginConfig → add → commitConfig → start的标准流程2. 预览帧处理第二路预览流需要实时处理帧数据用于小窗显示。这里用ImageReceiver监听帧到达事件function_startFrameLoop(receiver:image.ImageReceiver,w:number,h:number):void{receiver.on(imageArrival,(){receiver.readNextImage((err,nextImage){if(err||!nextImage)returnnextImage.getComponent(image.ComponentType.YUV_420_SP,async(e,comp){if(e||!comp?.byteBuffer){nextImage.release();return}try{conststridecomp.rowStrideletpm:image.PixelMap// 处理stride不等于width的情况if(stridew){pmawaitimage.createPixelMap(comp.byteBuffer,{size:{width:w,height:h},srcPixelFormat:image.PixelMapFormat.NV21})}else{pmawaitimage.createPixelMap(comp.byteBuffer,{size:{width:stride,height:h},srcPixelFormat:image.PixelMapFormat.NV21})pm.cropSync({x:0,y:0,size:{width:w,height:h}})}// 从竖向帧中心裁出16:9横向区域用于小窗显示constcropHMath.min(h,Math.round(w*9/16))constcropWMath.round(cropH*16/9)constxMath.round((w-cropW)/2)constyMath.round((h-cropH)/2)awaitpm.crop({x,y,size:{width:cropW,height:cropH}})AppStorage.setOrCreate(previewPixelMap,pm)}finally{nextImage.release()}})})})}注意点YUV数据的rowStride可能大于实际宽度需要先裁剪每次处理完必须调用nextImage.release()释放资源否则会内存泄漏小窗预览裁剪成16:9横向这样用户能看到横屏照片的大致效果3. 双拍摄核心逻辑这是整个功能的核心DualPhotoCapture类负责拍摄后的裁剪和保存exportclassDualPhotoCapture{asynccreateOutputs(mgr:camera.CameraManager,cap:camera.CameraOutputCapability):PromiseCaptureMode{constprofilescap.photoProfiles// 选择最高分辨率的拍摄配置constp1profiles.reduce((a,b)a.size.width*a.size.heightb.size.width*b.size.height?a:b)photoOutput1mgr.createPhotoOutput(p1)modecropAppStorage.setOrCreate(dualCaptureMode,mode)returnmode}addOutputsToSession(session:camera.PhotoSession):void{if(photoOutput1){session.addOutput(photoOutput1)// 监听照片拍摄完成事件photoOutput1.on(photoAvailable,(err:Error,photo:camera.Photo){if(err){hilog.error(0x0000,DualCamera,photo err: %{public}s,JSON.stringify(err))return}this._saveBoth(photo)})}}asynccapture():Promisevoid{awaitphotoOutput1?.capture()}}拍摄流程很简单选择最高分辨率的PhotoProfile创建PhotoOutput并添加到会话监听photoAvailable事件拍摄完成后调用_saveBoth处理照片4. 智能裁剪算法这是整个功能的精髓从一张照片裁出横竖两个版本privateasync_saveBoth(photo:camera.Photo):Promisevoid{try{// 获取JPEG数据constcomponentawaitphoto.main.getComponent(image.ComponentType.JPEG)constbuffer:ArrayBuffercomponent.byteBuffer// 获取原图尺寸constsrcimage.createImageSource(buffer)constpixelMapawaitsrc.createPixelMap()constinfo:image.ImageInfoawaitpixelMap.getImageInfo()constswinfo.size.widthconstshinfo.size.height// 竖屏裁剪9:16比例awaitthis._cropAndSave(buffer,sw,sh,9,16)// 横屏裁剪16:9比例awaitthis._cropAndSave(buffer,sw,sh,16,9)hilog.info(0x0000,DualCamera,both photos saved)}catch(e){hilog.error(0x0000,DualCamera,saveBoth error: %{public}s,JSON.stringify(e))}}关键在于_cropAndSave方法它能根据目标比例智能计算裁剪区域privateasync_cropAndSave(buffer:ArrayBuffer,sw:number,sh:number,ratioW:number,ratioH:number):Promisevoid{constsrcimage.createImageSource(buffer)constpixelMapawaitsrc.createPixelMap()letcropW:number,cropH:numberif(ratioWratioH){// 竖向裁剪宽度尽量大高度按比例计算cropWMath.min(sw,Math.round(sh*ratioW/ratioH))cropHMath.round(cropW*ratioH/ratioW)if(cropHsh){cropHsh cropWMath.round(sh*ratioW/ratioH)}}else{// 横向裁剪高度尽量大宽度按比例计算cropHMath.min(sh,Math.round(sw*ratioH/ratioW))cropWMath.round(cropH*ratioW/ratioH)if(cropWsw){cropWsw cropHMath.round(sw*ratioH/ratioW)}}// 居中裁剪constxMath.round((sw-cropW)/2)constyMath.round((sh-cropH)/2)awaitpixelMap.crop({x,y,size:{width:cropW,height:cropH}})// 打包成JPEG并保存constpackerimage.createImagePacker()constcroppedawaitpacker.packing(pixelMap,{format:image/jpeg,quality:95})awaitthis._writeToAlbum(cropped)}裁剪算法解析这个算法的核心思想是尽可能保留更多画面竖向裁剪9:16优先保证宽度最大然后按比例计算高度。如果计算出的高度超过原图就反过来以高度为准。横向裁剪16:9优先保证高度最大然后按比例计算宽度。同样有边界检查。居中裁剪计算出裁剪区域后从原图中心位置裁剪这样能保留画面的主体内容。举个例子假设原图是4000×30004:3比例裁剪竖屏9:16高度3000宽度3000×9/161687.5居中裁剪得到1688×3000裁剪横屏16:9高度3000宽度3000×16/95333超了改为宽度4000高度4000×9/162250居中裁剪得到4000×22505. 保存到相册保存逻辑使用MediaLibrary的photoAccessHelperprivateasync_writeToAlbum(buffer:ArrayBuffer):Promisevoid{consthelperphotoAccessHelper.getPhotoAccessHelper(getContext())consturiawaithelper.createAsset(photoAccessHelper.PhotoType.IMAGE,jpg)constfilefileIo.openSync(uri,fileIo.OpenMode.READ_WRITE)fileIo.writeSync(file.fd,buffer)fileIo.closeSync(file.fd)}这里用的是新的photoAccessHelper API比旧的mediaLibrary更简洁。注意需要在module.json5中申请权限requestPermissions:[{name:ohos.permission.CAMERA},{name:ohos.permission.WRITE_IMAGEVIDEO}]6. UI交互实现相机页面使用Stack布局分层显示预览、辅助线、小窗和按钮EntryComponentstruct CameraPage{StorageLink(previewPixelMap)previewPixelMap:image.PixelMap|nullnullprivatexComponentController:XComponentControllernewXComponentController()build(){Stack(){// 主预览 - XComponent显示相机画面XComponent({id:mainPreview,type:XComponentType.SURFACE,controller:this.xComponentController}).onLoad((){constsurfaceIdthis.xComponentController.getXComponentSurfaceId()initCamera(surfaceId)}).width(100%).height(100%)// 副预览小窗 - 显示横屏效果Image(this.previewPixelMap).width(120).height(90).objectFit(ImageFit.Fill).border({width:1,color:rgba(255,255,255,0.5)}).position({right:16,bottom:120})// 快门按钮Button({type:ButtonType.Circle}){Text(拍).fontColor(Color.Black).fontSize(18)}.width(72).height(72).backgroundColor(Color.White).position({bottom:32}).alignSelf(ItemAlign.Center).onClick(()capturePhoto())}.width(100%).height(100%).backgroundColor(Color.Black)}onPageHide():void{releaseCamera()}}UI设计要点主预览占满全屏提供沉浸式拍摄体验右下角小窗实时显示横屏裁剪效果让用户心里有数快门按钮居中底部符合用户习惯页面隐藏时释放相机资源避免占用实际效果实测下来这个方案效果不错速度快拍摄后裁剪保存两张照片整个过程不到500ms画质好从高分辨率原图裁剪画质无损兼容性强所有支持HarmonyOS相机的设备都能用用户体验好小窗实时预览让用户知道横屏照片的效果唯一的小问题是会占用一点存储空间因为保存了两张照片。不过现在手机存储都够大这点空间可以接受。优化建议如果要进一步优化可以考虑1. 异步处理现在裁剪和保存是串行的可以改成并行awaitPromise.all([this._cropAndSave(buffer,sw,sh,9,16),this._cropAndSave(buffer,sw,sh,16,9)])这样能再快一点不过实测提升不明显因为瓶颈在磁盘IO。2. 可配置比例现在固定是16:9和9:16可以让用户自定义比例比如1:1、4:5等社交媒体常用比例。3. 智能识别主体如果能用AI识别画面主体裁剪时优先保留主体而不是简单居中效果会更好。不过这个实现成本比较高。4. 添加水印或时间戳可以在保存前给照片加上水印或拍摄时间方便区分横竖版本。总结通过拍一张高分辨率照片智能裁剪的方案实现了一键同时拍摄横竖屏照片的功能。相比双PhotoOutput方案这个方案兼容性更好、性能更优、实现也更简单。核心要点回顾单PhotoOutput拍摄选择最高分辨率配置智能裁剪算法根据目标比例计算最优裁剪区域居中裁剪保留主体双预览流一路显示一路处理帧用于小窗预览异步保存裁剪后立即保存到相册这个方案在实际项目中已经稳定运行用户反馈不错。如果你也有类似需求可以参考这个思路实现。完整代码已上传到GitHub项目地址可以根据实际情况填写欢迎交流讨论。技术栈HarmonyOS 6.1 (API 23)ArkTSCamera KitImage KitMedia Library Kit关键APIcamera.PhotoSession- 相机会话管理image.ImageReceiver- 预览帧处理image.PixelMap.crop()- 图像裁剪photoAccessHelper- 相册访问