1# 适配不同折叠状态的摄像头变更(ArkTS) 2<!--Kit: Camera Kit--> 3<!--Subsystem: Multimedia--> 4<!--Owner: @qano--> 5<!--Designer: @leo_ysl--> 6<!--Tester: @xchaosioda--> 7<!--Adviser: @zengyawen--> 8折叠设备形态各异,在相机应用的开发过程中需要统一的摄像头切换方案,以确保用户在拍照、录像过程中获得更好的体验。 9 10一台可折叠设备在不同折叠状态下,可使用不同的相机。系统会标识所有摄像头,每个摄像头与一个折叠状态相对应,表示该摄像头可在对应的折叠状态下使用。应用可调用[CameraManager.on('foldStatusChange')](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#onfoldstatuschange12)或[display.on('foldStatusChange')](../../reference/apis-arkui/js-apis-display.md#displayonfoldstatuschange10)监听设备的折叠状态变化,并调用[CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras)获取当前状态下可用相机,完成相应适配,确保应用在折叠状态变更时的用户体验。 11 12不同折叠设备在不同折叠状态下支持的摄像头数量不同。 13 14例如,折叠设备A拥有三颗摄像头:B(后置)、C(前置)、D(前置)。在展开状态下,通过[CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras)接口可获取到B(后置)和C(前置)两颗摄像头,而在折叠状态下,仅可获取到D(前置)摄像头。因此,在使用后置摄像头或需要切换摄像头的场景下,需先判断是否存在后置摄像头。 15 16详细的API说明请参考[Camera API参考](../../reference/apis-camera-kit/arkts-apis-camera.md)。 17 18Context获取方式请参考:[获取UIAbility的上下文信息](../../application-models/uiability-usage.md#获取uiability的上下文信息)。 19 20在开发相机应用时,需要先申请相机相关权限,请参考[申请相关权限](camera-preparation.md)。 21## 创建XComponent 22 使用两个[XComponent](../../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)分别展示折叠态和展开态,防止切换折叠屏状态亮屏的时候上一个相机还未关闭,残留上一个相机的画面。 23 24 ```ts 25 @Entry 26 @Component 27 struct Index { 28 @State reloadXComponentFlag: boolean = false; 29 @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0; 30 private mXComponentController: XComponentController = new XComponentController(); 31 private mXComponentOptions: XComponentOptions = { 32 type: XComponentType.SURFACE, 33 controller: this.mXComponentController 34 } 35 36 reloadXComponent() { 37 this.reloadXComponentFlag = !this.reloadXComponentFlag; 38 } 39 40 async loadXComponent() { 41 //初始化XComponent。 42 } 43 44 build() { 45 Stack() { 46 if (this.reloadXComponentFlag) { 47 XComponent(this.mXComponentOptions) 48 .onLoad(async () => { 49 await this.loadXComponent(); 50 }) 51 .width(this.getUIContext().px2vp(1080)) 52 .height(this.getUIContext().px2vp(1920)) 53 } else { 54 XComponent(this.mXComponentOptions) 55 .onLoad(async () => { 56 await this.loadXComponent(); 57 }) 58 .width(this.getUIContext().px2vp(1080)) 59 .height(this.getUIContext().px2vp(1920)) 60 } 61 } 62 .size({ width: '100%', height: '100%' }) 63 .backgroundColor(Color.Black) 64 } 65 } 66 ``` 67## 获取设备折叠状态 68 69此处提供两种方案供开发者选择。 70 71- **方案一:使用相机框架提供的[CameraManager.on('foldStatusChange')](../../../application-dev/reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#onfoldstatuschange12)监听设备折叠态变化。** 72 ```ts 73 import { camera } from '@kit.CameraKit'; 74 import { BusinessError } from '@kit.BasicServicesKit'; 75 76 function registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) { 77 // foldStatus 变量用来控制显示XComponent组件。 78 AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus); 79 } 80 81 function onFoldStatusChange(cameraManager: camera.CameraManager) { 82 cameraManager.on('foldStatusChange', registerFoldStatusChanged); 83 } 84 85 function offFoldStatusChange(cameraManager: camera.CameraManager) { 86 cameraManager.off('foldStatusChange', registerFoldStatusChanged); 87 } 88 ``` 89- **方案二:使用图形图像的[display.on('foldStatusChange')](../../reference/apis-arkui/js-apis-display.md#displayonfoldstatuschange10)监听设备折叠态变化。** 90 ```ts 91 import { display } from '@kit.ArkUI'; 92 93 function getFoldStatus(): display.FoldStatus { 94 let curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN; 95 try { 96 curFoldStatus = display.getFoldStatus(); 97 } catch (error) { 98 console.error('getFoldStatus call failed'); 99 } 100 return curFoldStatus; 101 } 102 103 let preFoldStatus: display.FoldStatus = getFoldStatus(); 104 display.on('foldStatusChange', (foldStatus: display.FoldStatus) => { 105 // 从半折叠态(FOLD_STATUS_HALF_FOLDED)到展开态(FOLD_STATUS_EXPANDED),相机框架返回所支持的相机是一致的,所以从半折叠态到展开态不需要重新配流,从展开态到半折叠态也是一样的。 106 if ((preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED && 107 foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) || 108 (preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED && 109 foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) { 110 preFoldStatus = foldStatus; 111 return; 112 } 113 preFoldStatus = foldStatus; 114 // foldStatus 变量用来控制显示XComponent组件。 115 AppStorage.setOrCreate<number>('foldStatus', foldStatus); 116 }) 117 ``` 118## 判断是否存在对应位置摄像头 119通过[CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras)接口可获取到当前设备折叠状态下支持的所有镜头,遍历获取到的结果,通过[CameraPosition](../../reference/apis-camera-kit/arkts-apis-camera-e.md#cameraposition)判断镜头是否存在。 120```ts 121import { camera } from '@kit.CameraKit'; 122 123// connectionType默认为camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN,表示设备的内置镜头。 124function hasCameraAt(cameraManager: camera.CameraManager, cameraPosition: camera.CameraPosition, 125 connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): boolean { 126 let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras(); 127 if (cameraArray.length <= 0) { 128 console.error('cameraManager.getSupportedCameras error'); 129 return false; 130 } 131 for (let index = 0; index < cameraArray.length; index++) { 132 if (cameraArray[index].cameraPosition === cameraPosition && 133 cameraArray[index].connectionType === connectionType) { 134 return true; 135 } 136 } 137 return false; 138} 139``` 140## 摄像头切换逻辑 141在监听到折叠状态发生变化时通过设置被@StorageLink修饰的foldStatus变量改变,触发reloadXComponent方法重新加载XComponent组件,从而实现相机的切换逻辑。 142## 完整示例 143```ts 144import { camera } from '@kit.CameraKit'; 145import { BusinessError } from '@kit.BasicServicesKit'; 146import { abilityAccessCtrl } from '@kit.AbilityKit'; 147import { display } from '@kit.ArkUI'; 148 149const TAG = 'FoldScreenCameraAdaptationDemo '; 150 151@Entry 152@Component 153struct Index { 154 @State isShow: boolean = false; 155 @State reloadXComponentFlag: boolean = false; 156 @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0; 157 private mXComponentController: XComponentController = new XComponentController(); 158 private mXComponentOptions: XComponentOptions = { 159 type: XComponentType.SURFACE, 160 controller: this.mXComponentController 161 } 162 private mSurfaceId: string = ''; 163 private mCameraPosition: camera.CameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK; 164 private mCameraManager: camera.CameraManager | undefined = undefined; 165 // surface宽高根据需要自行选择。 166 private surfaceRect: SurfaceRect = { 167 surfaceWidth: 1080, 168 surfaceHeight: 1920 169 }; 170 private curCameraDevice: camera.CameraDevice | undefined = undefined; 171 private mCameraInput: camera.CameraInput | undefined = undefined; 172 private mPreviewOutput: camera.PreviewOutput | undefined = undefined; 173 private mPhotoSession: camera.PhotoSession | undefined = undefined; 174 // 请根据实际业务诉求选择符合需求场景的预览流Profile,此处以分辨率1080P,CameraFormat:1003为例。 175 private previewProfileObj: camera.Profile = { 176 format: 1003, 177 size: { 178 width: 1920, 179 height: 1080 180 } 181 }; 182 private mContext: Context | undefined = undefined; 183 184 private preFoldStatus: display.FoldStatus = this.getFoldStatus(); 185 // 监听折叠屏状态,可以使用cameraManager.on(type: 'foldStatusChange', callback: AsyncCallback<FoldStatusInfo>): void; 186 // 也可以使用display.on(type: 'foldStatusChange', callback: Callback<FoldStatus>): void; 187 private foldStatusCallback = 188 (err: BusinessError, info: camera.FoldStatusInfo): void => this.registerFoldStatusChanged(err, info); 189 private displayFoldStatusCallback = 190 (foldStatus: display.FoldStatus): void => this.onDisplayFoldStatusChange(foldStatus); 191 192 getFoldStatus(): display.FoldStatus { 193 let curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN; 194 try { 195 curFoldStatus = display.getFoldStatus(); 196 } catch (error) { 197 console.info(`${TAG} getFoldStatus call failed, error: ${error.code}`); 198 } 199 return curFoldStatus; 200 } 201 202 registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) { 203 if (err !== undefined && err.code !== 0) { 204 console.info(`${TAG} registerFoldStatusChanged call failed, error: ${err.code}`); 205 return; 206 } 207 if (foldStatusInfo && foldStatusInfo.supportedCameras) { 208 console.info(`${TAG} foldStatusChanged foldStatus: ${foldStatusInfo.foldStatus}`); 209 for (let i = 0; i < foldStatusInfo.supportedCameras.length; i++) { 210 console.info(TAG + 211 `foldStatusChanged camera[${i}]: ${foldStatusInfo.supportedCameras[i].cameraId},cameraPosition: ${foldStatusInfo.supportedCameras[i].cameraPosition}`); 212 } 213 } 214 AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus); 215 } 216 217 onDisplayFoldStatusChange(foldStatus: display.FoldStatus): void { 218 console.info(TAG + `onDisplayFoldStatusChange foldStatus: ${foldStatus}`); 219 if ((this.preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED && 220 foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) || 221 (this.preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED && 222 foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) { 223 this.preFoldStatus = foldStatus; 224 return; 225 } 226 this.preFoldStatus = foldStatus; 227 if (!this.curCameraDevice) { 228 return; 229 } 230 // foldStatus 变量用来控制显示XComponent组件。 231 AppStorage.setOrCreate<number>('foldStatus', foldStatus); 232 } 233 234 requestPermissionsFn(): void { 235 let atManager = abilityAccessCtrl.createAtManager(); 236 atManager.requestPermissionsFromUser(this.mContext, [ 237 'ohos.permission.CAMERA' 238 ]).then((): void => { 239 this.isShow = true; 240 }).catch((error: BusinessError): void => { 241 console.error(`${TAG} requestPermissionsFromUser call failed, error: ${error.code}`); 242 }); 243 } 244 245 initContext(): void { 246 let uiContext = this.getUIContext(); 247 this.mContext = uiContext.getHostContext(); 248 } 249 250 initCameraManager(): void { 251 try { 252 this.mCameraManager = camera.getCameraManager(this.mContext); 253 } catch (error) { 254 console.error(`${TAG} getCameraManager call failed, error: ${error.code}`); 255 } 256 } 257 258 aboutToAppear(): void { 259 console.log(TAG + 'aboutToAppear is called'); 260 this.initContext(); 261 this.initCameraManager(); 262 this.requestPermissionsFn(); 263 this.onFoldStatusChange(); 264 } 265 266 async aboutToDisappear(): Promise<void> { 267 await this.releaseCamera(); 268 // 解注册。 269 this.offFoldStatusChange(); 270 } 271 272 async onPageShow(): Promise<void> { 273 await this.initCamera(this.mSurfaceId, this.mCameraPosition); 274 } 275 276 async releaseCamera(): Promise<void> { 277 // 停止当前会话。 278 try { 279 await this.mPhotoSession?.stop(); 280 } catch (error) { 281 let err = error as BusinessError; 282 console.error(TAG + 'Failed to stop session, errorCode = ' + err.code); 283 } 284 285 // 释放相机输入流。 286 try { 287 await this.mCameraInput?.close(); 288 } catch (error) { 289 let err = error as BusinessError; 290 console.error(TAG + 'Failed to close device, errorCode = ' + err.code); 291 } 292 293 // 释放预览输出流。 294 try { 295 await this.mPreviewOutput?.release(); 296 } catch (error) { 297 let err = error as BusinessError; 298 console.error(TAG + 'Failed to release previewOutput, errorCode = ' + err.code); 299 } 300 301 this.mPreviewOutput = undefined; 302 303 // 释放会话。 304 try { 305 await this.mPhotoSession?.release(); 306 } catch (error) { 307 let err = error as BusinessError; 308 console.error(TAG + 'Failed to release photoSession, errorCode = ' + err.code); 309 } 310 311 // 会话置空。 312 this.mPhotoSession = undefined; 313 } 314 315 onFoldStatusChange(): void { 316 this.mCameraManager?.on('foldStatusChange', this.foldStatusCallback); 317 // display.on('foldStatusChange', this.displayFoldStatusCallback); 318 } 319 320 offFoldStatusChange(): void { 321 this.mCameraManager?.off('foldStatusChange', this.foldStatusCallback); 322 // display.off('foldStatusChange', this.displayFoldStatusCallback); 323 } 324 325 reloadXComponent(): void { 326 this.reloadXComponentFlag = !this.reloadXComponentFlag; 327 } 328 329 async loadXComponent(): Promise<void> { 330 this.mSurfaceId = this.mXComponentController.getXComponentSurfaceId(); 331 this.mXComponentController.setXComponentSurfaceRect(this.surfaceRect); 332 console.info(TAG + `mCameraPosition: ${this.mCameraPosition}`) 333 await this.initCamera(this.mSurfaceId, this.mCameraPosition); 334 } 335 336 getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { 337 let previewProfiles = cameraOutputCapability.previewProfiles; 338 if (previewProfiles.length < 1) { 339 return undefined; 340 } 341 let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { 342 return previewProfile.size.width === this.previewProfileObj.size.width && 343 previewProfile.size.height === this.previewProfileObj.size.height && 344 previewProfile.format === this.previewProfileObj.format; 345 }) 346 if (index === -1) { 347 return undefined; 348 } 349 return previewProfiles[index]; 350 } 351 352 async initCamera(surfaceId: string, cameraPosition: camera.CameraPosition, 353 connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): Promise<void> { 354 await this.releaseCamera(); 355 // 创建CameraManager对象。 356 if (!this.mCameraManager) { 357 console.error(TAG + 'camera.getCameraManager error'); 358 return; 359 } 360 361 // 获取相机列表。 362 let cameraArray: Array<camera.CameraDevice> = this.mCameraManager.getSupportedCameras(); 363 if (cameraArray.length <= 0) { 364 console.error(TAG + 'cameraManager.getSupportedCameras error'); 365 return; 366 } 367 368 for (let index = 0; index < cameraArray.length; index++) { 369 console.info(TAG + 'cameraId : ' + cameraArray[index].cameraId); // 获取相机ID。 370 console.info(TAG + 'cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置。 371 console.info(TAG + 'cameraType : ' + cameraArray[index].cameraType); // 获取相机类型。 372 console.info(TAG + 'connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型。 373 } 374 375 let deviceIndex = cameraArray.findIndex((cameraDevice: camera.CameraDevice) => { 376 return cameraDevice.cameraPosition === cameraPosition && cameraDevice.connectionType === connectionType; 377 }) 378 // 没有找到对应位置的摄像头,可选择其他摄像头,具体场景具体对待。 379 if (deviceIndex === -1) { 380 deviceIndex = 0; 381 console.error(TAG + 'not found camera'); 382 } 383 this.curCameraDevice = cameraArray[deviceIndex]; 384 385 // 创建相机输入流。 386 try { 387 this.mCameraInput = this.mCameraManager.createCameraInput(this.curCameraDevice); 388 } catch (error) { 389 let err = error as BusinessError; 390 console.error(TAG + 'Failed to createCameraInput errorCode = ' + err.code); 391 } 392 if (this.mCameraInput === undefined) { 393 return; 394 } 395 396 // 打开相机。 397 try { 398 await this.mCameraInput.open(); 399 } catch (error) { 400 let err = error as BusinessError; 401 console.error(TAG + 'Failed to open device, errorCode = ' + err.code); 402 } 403 404 // 获取支持的模式类型。 405 let sceneModes: Array<camera.SceneMode> = this.mCameraManager.getSupportedSceneModes(this.curCameraDevice); 406 let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0; 407 if (!isSupportPhotoMode) { 408 console.error(TAG + 'photo mode not support'); 409 return; 410 } 411 412 // 获取相机设备支持的输出流能力。 413 let cameraOutputCapability: camera.CameraOutputCapability = 414 this.mCameraManager.getSupportedOutputCapability(this.curCameraDevice, camera.SceneMode.NORMAL_PHOTO); 415 if (!cameraOutputCapability) { 416 console.error(TAG + 'cameraManager.getSupportedOutputCapability error'); 417 return; 418 } 419 console.info(TAG + 'outputCapability: ' + JSON.stringify(cameraOutputCapability)); 420 let previewProfile = this.getPreviewProfile(cameraOutputCapability); 421 if (previewProfile === undefined) { 422 console.error(TAG + 'The resolution of the current preview stream is not supported.'); 423 return; 424 } 425 this.previewProfileObj = previewProfile; 426 427 // 创建预览输出流,其中参数 surfaceId 参考上文 XComponent 组件,预览流为XComponent组件提供的surface。 428 try { 429 this.mPreviewOutput = this.mCameraManager.createPreviewOutput(this.previewProfileObj, surfaceId); 430 } catch (error) { 431 let err = error as BusinessError; 432 console.error(TAG + `Failed to create the PreviewOutput instance. error code: ${err.code}`); 433 } 434 if (this.mPreviewOutput === undefined) { 435 return; 436 } 437 438 //创建会话。 439 try { 440 this.mPhotoSession = this.mCameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; 441 } catch (error) { 442 let err = error as BusinessError; 443 console.error(TAG + 'Failed to create the session instance. errorCode = ' + err.code); 444 } 445 if (this.mPhotoSession === undefined) { 446 return; 447 } 448 449 // 开始配置会话。 450 try { 451 this.mPhotoSession.beginConfig(); 452 } catch (error) { 453 let err = error as BusinessError; 454 console.error(TAG + 'Failed to beginConfig. errorCode = ' + err.code); 455 } 456 457 // 向会话中添加相机输入流。 458 try { 459 this.mPhotoSession.addInput(this.mCameraInput); 460 } catch (error) { 461 let err = error as BusinessError; 462 console.error(TAG + 'Failed to addInput. errorCode = ' + err.code); 463 } 464 465 // 向会话中添加预览输出流。 466 try { 467 this.mPhotoSession.addOutput(this.mPreviewOutput); 468 } catch (error) { 469 let err = error as BusinessError; 470 console.error(TAG + 'Failed to addOutput(previewOutput). errorCode = ' + err.code); 471 } 472 473 // 提交会话配置。 474 try { 475 await this.mPhotoSession.commitConfig(); 476 } catch (error) { 477 let err = error as BusinessError; 478 console.error(TAG + 'Failed to commit session configuration, errorCode = ' + err.code); 479 } 480 481 // 启动会话。 482 try { 483 await this.mPhotoSession.start() 484 } catch (error) { 485 let err = error as BusinessError; 486 console.error(TAG + 'Failed to start session. errorCode = ' + err.code); 487 } 488 } 489 490 build() { 491 if (this.isShow) { 492 Stack() { 493 if (this.reloadXComponentFlag) { 494 XComponent(this.mXComponentOptions) 495 .onLoad(async () => { 496 await this.loadXComponent(); 497 }) 498 .width(this.getUIContext().px2vp(1080)) 499 .height(this.getUIContext().px2vp(1920)) 500 } else { 501 XComponent(this.mXComponentOptions) 502 .onLoad(async () => { 503 await this.loadXComponent(); 504 }) 505 .width(this.getUIContext().px2vp(1080)) 506 .height(this.getUIContext().px2vp(1920)) 507 } 508 Text('切换相机') 509 .size({ width: 80, height: 48 }) 510 .position({ x: 1, y: 1 }) 511 .backgroundColor(Color.White) 512 .textAlign(TextAlign.Center) 513 .borderRadius(24) 514 .onClick(async () => { 515 this.mCameraPosition = this.mCameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK ? 516 camera.CameraPosition.CAMERA_POSITION_FRONT : camera.CameraPosition.CAMERA_POSITION_BACK; 517 this.reloadXComponentFlag = !this.reloadXComponentFlag; 518 }) 519 } 520 .size({ width: '100%', height: '100%' }) 521 .backgroundColor(Color.Black) 522 } 523 } 524} 525``` 526