1# Adapting Camera Changes in Different Fold States (ArkTS) 2<!--Kit: Camera Kit--> 3<!--Subsystem: Multimedia--> 4<!--Owner: @qano--> 5<!--SE: @leo_ysl--> 6<!--TSE: @xchaosioda--> 7Foldable devices come in various forms. When developing camera applications, a consistent camera switching solution is necessary to enhance user experience during photo and video capture. 8 9A single foldable device can use different cameras depending on its fold state. The system identifies each camera and associates it with a specific fold state, indicating which cameras are available in those states. Applications can call [CameraManager.on('foldStatusChange')](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#onfoldstatuschange12) or [display.on('foldStatusChange')](../../reference/apis-arkui/js-apis-display.md#displayonfoldstatuschange10) to listen for fold state changes of the device, call [CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras) to obtain the available cameras in the current state, and make adaptations accordingly. 10 11The number of supported cameras can differ among foldable devices in various fold states. 12 13For example, foldable device A has three cameras: B (rear), C (front), and D (front). In the unfolded state, calling [CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras) returns both cameras B (rear) and C (front). However, in the folded state, only camera D (front) is accessible. Therefore, when using the rear camera or switching between cameras, it is crucial to first verify the existence of the rear camera. 14 15Read [Module Description](../../reference/apis-camera-kit/arkts-apis-camera.md) for the API reference. 16 17For details about how to obtain the context, see [Obtaining the Context of UIAbility](../../application-models/uiability-usage.md#obtaining-the-context-of-uiability). 18 19Before developing a camera application, request permissions by following the instructions provided in [Requesting Camera Development Permissions](camera-preparation.md). 20## Creating an XComponent 21Use two [XComponents](../../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md) to present the folded and unfolded states, respectively. This prevents the previous camera feed from lingering on the screen if the camera is not properly closed during fold state transition. 22 23 ```ts 24 @Entry 25 @Component 26 struct Index { 27 @State reloadXComponentFlag: boolean = false; 28 @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0; 29 private mXComponentController: XComponentController = new XComponentController(); 30 private mXComponentOptions: XComponentOptions = { 31 type: XComponentType.SURFACE, 32 controller: this.mXComponentController 33 } 34 35 reloadXComponent() { 36 this.reloadXComponentFlag = !this.reloadXComponentFlag; 37 } 38 39 async loadXComponent() { 40 // Initialize the XComponent. 41 } 42 43 build() { 44 Stack() { 45 if (this.reloadXComponentFlag) { 46 XComponent(this.mXComponentOptions) 47 .onLoad(async () => { 48 await this.loadXComponent(); 49 }) 50 .width(this.getUIContext().px2vp(1080)) 51 .height(this.getUIContext().px2vp(1920)) 52 } else { 53 XComponent(this.mXComponentOptions) 54 .onLoad(async () => { 55 await this.loadXComponent(); 56 }) 57 .width(this.getUIContext().px2vp(1080)) 58 .height(this.getUIContext().px2vp(1920)) 59 } 60 } 61 .size({ width: '100%', height: '100%' }) 62 .backgroundColor(Color.Black) 63 } 64 } 65 ``` 66## Obtaining the Device Fold State 67 68You can use either of the following solutions. 69 70- Solution 1: Call [CameraManager.on('foldStatusChange')](../../../application-dev/reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#onfoldstatuschange12) provided by the camera framework to listen for fold state changes. 71 ```ts 72 import { camera } from '@kit.CameraKit'; 73 import { BusinessError } from '@kit.BasicServicesKit'; 74 75 function registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) { 76 // The foldStatus variable is used to control the display of the XComponent. 77 AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus); 78 } 79 80 function onFoldStatusChange(cameraManager: camera.CameraManager) { 81 cameraManager.on('foldStatusChange', registerFoldStatusChanged); 82 } 83 84 function offFoldStatusChange(cameraManager: camera.CameraManager) { 85 cameraManager.off('foldStatusChange', registerFoldStatusChanged); 86 } 87 ``` 88- Solution 2: Call [display.on('foldStatusChange')](../../reference/apis-arkui/js-apis-display.md#displayonfoldstatuschange10) to listen for fold state changes. 89 ```ts 90 import { display } from '@kit.ArkUI'; 91 let preFoldStatus: display.FoldStatus = display.getFoldStatus(); 92 display.on('foldStatusChange', (foldStatus: display.FoldStatus) => { 93 // The supported cameras returned by the camera framework are the same when the device is in the FOLD_STATUS_HALF_FOLDED or FOLD_STATUS_EXPANDED state. Therefore, you do not need to reconfigure streams during the transition between these two states. 94 if ((preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED && 95 foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) || 96 (preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED && 97 foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) { 98 preFoldStatus = foldStatus; 99 return; 100 } 101 preFoldStatus = foldStatus; 102 // The foldStatus variable is used to control the display of the XComponent. 103 AppStorage.setOrCreate<number>('foldStatus', foldStatus); 104 }) 105 ``` 106## Checking the Presence of a Camera at a Specific Position 107You can call [CameraManager.getSupportedCameras](../../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras) to obtain all the cameras supported by the device in the current fold state. By iterating through the results and using [CameraPosition](../../reference/apis-camera-kit/arkts-apis-camera-e.md#cameraposition), you can determine whether a camera exists at the specified position. 108```ts 109// The default value of connectionType is camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN, indicating the device's built-in camera. 110function hasCameraAt(cameraManager: camera.CameraManager, cameraPosition: camera.CameraPosition, 111 connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): boolean { 112 let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras(); 113 if (cameraArray.length <= 0) { 114 console.error('cameraManager.getSupportedCameras error'); 115 return false; 116 } 117 for (let index = 0; index < cameraArray.length; index++) { 118 if (cameraArray[index].cameraPosition === cameraPosition && 119 cameraArray[index].connectionType === connectionType) { 120 return true; 121 } 122 } 123 return false; 124} 125``` 126## Camera Switching Logic 127When a fold state change is detected, the **foldStatus** variable, decorated with @StorageLink, is updated. This triggers the **reloadXComponent** API to reload the **XComponent**, thereby implementing the camera switching logic. 128## Sample Code 129```ts 130import { camera } from '@kit.CameraKit'; 131import { BusinessError } from '@kit.BasicServicesKit'; 132import { abilityAccessCtrl } from '@kit.AbilityKit'; 133import { display } from '@kit.ArkUI'; 134 135const TAG = 'FoldScreenCameraAdaptationDemo '; 136 137@Entry 138@Component 139struct Index { 140 @State isShow: boolean = false; 141 @State reloadXComponentFlag: boolean = false; 142 @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0; 143 private mXComponentController: XComponentController = new XComponentController(); 144 private mXComponentOptions: XComponentOptions = { 145 type: XComponentType.SURFACE, 146 controller: this.mXComponentController 147 } 148 private mSurfaceId: string = ''; 149 private mCameraPosition: camera.CameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK; 150 private mCameraManager: camera.CameraManager | undefined = undefined; 151 // Select the surface width and height as required. 152 private surfaceRect: SurfaceRect = { 153 surfaceWidth: 1080, 154 surfaceHeight: 1920 155 }; 156 private curCameraDevice: camera.CameraDevice | undefined = undefined; 157 private mCameraInput: camera.CameraInput | undefined = undefined; 158 private mPreviewOutput: camera.PreviewOutput | undefined = undefined; 159 private mPhotoSession: camera.PhotoSession | undefined = undefined; 160 // One of the recommended preview resolutions. 161 private previewProfileObj: camera.Profile = { 162 format: 1003, 163 size: { 164 width: 1920, 165 height: 1080 166 } 167 }; 168 private mContext: Context | undefined = undefined; 169 170 private preFoldStatus: display.FoldStatus = display.getFoldStatus(); 171 // Listen for the foldable screen status. You can use cameraManager.on(type: 'foldStatusChange', callback: AsyncCallback<FoldStatusInfo>): void; 172 // or display.on(type: 'foldStatusChange', callback: Callback<FoldStatus>): void;. 173 private foldStatusCallback = 174 (err: BusinessError, info: camera.FoldStatusInfo): void => this.registerFoldStatusChanged(err, info); 175 private displayFoldStatusCallback = 176 (foldStatus: display.FoldStatus): void => this.onDisplayFoldStatusChange(foldStatus); 177 178 179 registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) { 180 console.info(TAG + 'foldStatusChanged foldStatus: ' + foldStatusInfo.foldStatus); 181 for (let i = 0; i < foldStatusInfo.supportedCameras.length; i++) { 182 console.info(TAG + 183 `foldStatusChanged camera[${i}]: ${foldStatusInfo.supportedCameras[i].cameraId},cameraPosition: ${foldStatusInfo.supportedCameras[i].cameraPosition}`); 184 } 185 AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus); 186 } 187 188 onDisplayFoldStatusChange(foldStatus: display.FoldStatus): void { 189 console.error(TAG + `onDisplayFoldStatusChange foldStatus: ${foldStatus}`); 190 if ((this.preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED && 191 foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) || 192 (this.preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED && 193 foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) { 194 this.preFoldStatus = foldStatus; 195 return; 196 } 197 this.preFoldStatus = foldStatus; 198 if (!this.curCameraDevice) { 199 return; 200 } 201 // The foldStatus variable is used to control the display of the XComponent. 202 AppStorage.setOrCreate<number>('foldStatus', foldStatus); 203 } 204 205 requestPermissionsFn(): void { 206 let atManager = abilityAccessCtrl.createAtManager(); 207 atManager.requestPermissionsFromUser(this.mContext, [ 208 'ohos.permission.CAMERA' 209 ]).then((): void => { 210 this.isShow = true; 211 }).catch((error: BusinessError): void => { 212 console.error(TAG + 'ohos.permission.CAMERA no permission.'); 213 }); 214 } 215 216 initContext(): void { 217 let uiContext = this.getUIContext(); 218 this.mContext = uiContext.getHostContext(); 219 } 220 221 initCameraManager(): void { 222 this.mCameraManager = camera.getCameraManager(this.mContext); 223 } 224 225 aboutToAppear(): void { 226 console.log(TAG + 'aboutToAppear is called'); 227 this.initContext(); 228 this.initCameraManager(); 229 this.requestPermissionsFn(); 230 this.onFoldStatusChange(); 231 } 232 233 async aboutToDisappear(): Promise<void> { 234 await this.releaseCamera(); 235 // Stop the listening. 236 this.offFoldStatusChange(); 237 } 238 239 async onPageShow(): Promise<void> { 240 await this.initCamera(this.mSurfaceId, this.mCameraPosition); 241 } 242 243 async releaseCamera(): Promise<void> { 244 // Stop the session. 245 try { 246 await this.mPhotoSession?.stop(); 247 } catch (error) { 248 let err = error as BusinessError; 249 console.error(TAG + 'Failed to stop session, errorCode = ' + err.code); 250 } 251 252 // Release the camera input stream. 253 try { 254 await this.mCameraInput?.close(); 255 } catch (error) { 256 let err = error as BusinessError; 257 console.error(TAG + 'Failed to close device, errorCode = ' + err.code); 258 } 259 260 // Release the preview output stream. 261 try { 262 await this.mPreviewOutput?.release(); 263 } catch (error) { 264 let err = error as BusinessError; 265 console.error(TAG + 'Failed to release previewOutput, errorCode = ' + err.code); 266 } 267 268 this.mPreviewOutput = undefined; 269 270 // Release the session. 271 try { 272 await this.mPhotoSession?.release(); 273 } catch (error) { 274 let err = error as BusinessError; 275 console.error(TAG + 'Failed to release photoSession, errorCode = ' + err.code); 276 } 277 278 // Set the session to null. 279 this.mPhotoSession = undefined; 280 } 281 282 onFoldStatusChange(): void { 283 this.mCameraManager?.on('foldStatusChange', this.foldStatusCallback); 284 // display.on('foldStatusChange', this.displayFoldStatusCallback); 285 } 286 287 offFoldStatusChange(): void { 288 this.mCameraManager?.off('foldStatusChange', this.foldStatusCallback); 289 // display.off('foldStatusChange', this.displayFoldStatusCallback); 290 } 291 292 reloadXComponent(): void { 293 this.reloadXComponentFlag = !this.reloadXComponentFlag; 294 } 295 296 async loadXComponent(): Promise<void> { 297 this.mSurfaceId = this.mXComponentController.getXComponentSurfaceId(); 298 this.mXComponentController.setXComponentSurfaceRect(this.surfaceRect); 299 console.info(TAG + `mCameraPosition: ${this.mCameraPosition}`) 300 await this.initCamera(this.mSurfaceId, this.mCameraPosition); 301 } 302 303 getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { 304 let previewProfiles = cameraOutputCapability.previewProfiles; 305 if (previewProfiles.length < 1) { 306 return undefined; 307 } 308 let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { 309 return previewProfile.size.width === this.previewProfileObj.size.width && 310 previewProfile.size.height === this.previewProfileObj.size.height && 311 previewProfile.format === this.previewProfileObj.format; 312 }) 313 if (index === -1) { 314 return undefined; 315 } 316 return previewProfiles[index]; 317 } 318 319 async initCamera(surfaceId: string, cameraPosition: camera.CameraPosition, 320 connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): Promise<void> { 321 await this.releaseCamera(); 322 // Create a CameraManager object. 323 if (!this.mCameraManager) { 324 console.error(TAG + 'camera.getCameraManager error'); 325 return; 326 } 327 328 // Obtain the camera list. 329 let cameraArray: Array<camera.CameraDevice> = this.mCameraManager.getSupportedCameras(); 330 if (cameraArray.length <= 0) { 331 console.error(TAG + 'cameraManager.getSupportedCameras error'); 332 return; 333 } 334 335 for (let index = 0; index < cameraArray.length; index++) { 336 console.info(TAG + 'cameraId : ' + cameraArray[index].cameraId); // Obtain the camera ID. 337 console.info(TAG + 'cameraPosition : ' + cameraArray[index].cameraPosition); // Obtain the camera position. 338 console.info(TAG + 'cameraType : ' + cameraArray[index].cameraType); // Obtain the camera type. 339 console.info(TAG + 'connectionType : ' + cameraArray[index].connectionType); // Obtain the camera connection type. 340 } 341 342 let deviceIndex = cameraArray.findIndex((cameraDevice: camera.CameraDevice) => { 343 return cameraDevice.cameraPosition === cameraPosition && cameraDevice.connectionType === connectionType; 344 }) 345 // If no camera is found at the specified position, you can select another camera. Handle the situation based on the specific scenario. 346 if (deviceIndex === -1) { 347 deviceIndex = 0; 348 console.error(TAG + 'not found camera'); 349 } 350 this.curCameraDevice = cameraArray[deviceIndex]; 351 352 // Create a camera input stream. 353 try { 354 this.mCameraInput = this.mCameraManager.createCameraInput(this.curCameraDevice); 355 } catch (error) { 356 let err = error as BusinessError; 357 console.error(TAG + 'Failed to createCameraInput errorCode = ' + err.code); 358 } 359 if (this.mCameraInput === undefined) { 360 return; 361 } 362 363 // Open the camera. 364 try { 365 await this.mCameraInput.open(); 366 } catch (error) { 367 let err = error as BusinessError; 368 console.error(TAG + 'Failed to open device, errorCode = ' + err.code); 369 } 370 371 // Obtain the supported modes. 372 let sceneModes: Array<camera.SceneMode> = this.mCameraManager.getSupportedSceneModes(this.curCameraDevice); 373 let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0; 374 if (!isSupportPhotoMode) { 375 console.error(TAG + 'photo mode not support'); 376 return; 377 } 378 379 // Obtain the output stream capability supported by the camera. 380 let cameraOutputCapability: camera.CameraOutputCapability = 381 this.mCameraManager.getSupportedOutputCapability(this.curCameraDevice, camera.SceneMode.NORMAL_PHOTO); 382 if (!cameraOutputCapability) { 383 console.error(TAG + 'cameraManager.getSupportedOutputCapability error'); 384 return; 385 } 386 console.info(TAG + 'outputCapability: ' + JSON.stringify(cameraOutputCapability)); 387 let previewProfile = this.getPreviewProfile(cameraOutputCapability); 388 if (previewProfile === undefined) { 389 console.error(TAG + 'The resolution of the current preview stream is not supported.'); 390 return; 391 } 392 this.previewProfileObj = previewProfile; 393 394 // Create a preview output stream. For details about the surfaceId parameter, see the XComponent. The preview stream uses the surface provided by the XComponent. 395 try { 396 this.mPreviewOutput = this.mCameraManager.createPreviewOutput(this.previewProfileObj, surfaceId); 397 } catch (error) { 398 let err = error as BusinessError; 399 console.error(TAG + `Failed to create the PreviewOutput instance. error code: ${err.code}`); 400 } 401 if (this.mPreviewOutput === undefined) { 402 return; 403 } 404 405 // Create a session. 406 try { 407 this.mPhotoSession = this.mCameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; 408 } catch (error) { 409 let err = error as BusinessError; 410 console.error(TAG + 'Failed to create the session instance. errorCode = ' + err.code); 411 } 412 if (this.mPhotoSession === undefined) { 413 return; 414 } 415 416 // Start configuration for the session. 417 try { 418 this.mPhotoSession.beginConfig(); 419 } catch (error) { 420 let err = error as BusinessError; 421 console.error(TAG + 'Failed to beginConfig. errorCode = ' + err.code); 422 } 423 424 // Add the camera input stream to the session. 425 try { 426 this.mPhotoSession.addInput(this.mCameraInput); 427 } catch (error) { 428 let err = error as BusinessError; 429 console.error(TAG + 'Failed to addInput. errorCode = ' + err.code); 430 } 431 432 // Add the preview output stream to the session. 433 try { 434 this.mPhotoSession.addOutput(this.mPreviewOutput); 435 } catch (error) { 436 let err = error as BusinessError; 437 console.error(TAG + 'Failed to addOutput(previewOutput). errorCode = ' + err.code); 438 } 439 440 // Commit the session configuration. 441 try { 442 await this.mPhotoSession.commitConfig(); 443 } catch (error) { 444 let err = error as BusinessError; 445 console.error(TAG + 'Failed to commit session configuration, errorCode = ' + err.code); 446 } 447 448 // Start the session. 449 try { 450 await this.mPhotoSession.start() 451 } catch (error) { 452 let err = error as BusinessError; 453 console.error(TAG + 'Failed to start session. errorCode = ' + err.code); 454 } 455 } 456 457 build() { 458 if (this.isShow) { 459 Stack() { 460 if (this.reloadXComponentFlag) { 461 XComponent(this.mXComponentOptions) 462 .onLoad(async () => { 463 await this.loadXComponent(); 464 }) 465 .width(this.getUIContext().px2vp(1080)) 466 .height(this.getUIContext().px2vp(1920)) 467 } else { 468 XComponent(this.mXComponentOptions) 469 .onLoad(async () => { 470 await this.loadXComponent(); 471 }) 472 .width(this.getUIContext().px2vp(1080)) 473 .height(this.getUIContext().px2vp(1920)) 474 } 475 Text('Switch camera') 476 .size({ width: 80, height: 48 }) 477 .position({ x: 1, y: 1 }) 478 .backgroundColor(Color.White) 479 .textAlign(TextAlign.Center) 480 .borderRadius(24) 481 .onClick(async () => { 482 this.mCameraPosition = this.mCameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK ? 483 camera.CameraPosition.CAMERA_POSITION_FRONT : camera.CameraPosition.CAMERA_POSITION_BACK; 484 this.reloadXComponentFlag = !this.reloadXComponentFlag; 485 }) 486 } 487 .size({ width: '100%', height: '100%' }) 488 .backgroundColor(Color.Black) 489 } 490 } 491} 492``` 493