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