• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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