We use essential cookies for the website to function, as well as analytics cookies for analyzing and creating statistics of the website performance. To agree to the use of analytics cookies, click "Accept All". You can manage your preferences at any time by clicking "Cookie Settings" on the footer. More Information.

Only Essential Cookies
Accept All
GuidesMediaCamera KitFundamental Capabilities for Camera Application Development (ArkTS)Adapting Camera Changes in Different Fold States (ArkTS)

Adapting Camera Changes in Different Fold States (ArkTS)

Foldable 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.

A 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') or display.on('foldStatusChange') to listen for fold state changes of the device, call CameraManager.getSupportedCameras to obtain the available cameras in the current state, and make adaptations accordingly.

The number of supported cameras can differ among foldable devices in various fold states.

For example, a foldable device has three cameras: rear camera A, front camera B, and front camera C. In the unfolded state, calling CameraManager.getSupportedCameras returns rear camera A and front camera B; in the folded state, it returns rear camera A and front camera C. The cameras available differ between fold states, so you must re-obtain camera information when the fold state changes.

For detailed API descriptions, see @ohos.multimedia.camera (Camera Management).

For details about how to obtain the context, see Obtaining the Context of UIAbility.

Before developing a camera application, you must request required permissions.

Creating an XComponent

Use two XComponents 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.

Collapse
Word wrap
Dark theme
Copy code
  1. @Entry
  2. @Component
  3. struct Index {
  4. @State reloadXComponentFlag: boolean = false;
  5. @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0;
  6. private mXComponentController: XComponentController = new XComponentController();
  7. private mXComponentOptions: XComponentOptions = {
  8. type: XComponentType.SURFACE,
  9. controller: this.mXComponentController
  10. }
  11. reloadXComponent() {
  12. this.reloadXComponentFlag = !this.reloadXComponentFlag;
  13. }
  14. async loadXComponent() {
  15. // Initialize the XComponent.
  16. }
  17. build() {
  18. Stack() {
  19. if (this.reloadXComponentFlag) {
  20. XComponent(this.mXComponentOptions)
  21. .onLoad(async () => {
  22. await this.loadXComponent();
  23. })
  24. .width(this.getUIContext().px2vp(1080))
  25. .height(this.getUIContext().px2vp(1920))
  26. } else {
  27. XComponent(this.mXComponentOptions)
  28. .onLoad(async () => {
  29. await this.loadXComponent();
  30. })
  31. .width(this.getUIContext().px2vp(1080))
  32. .height(this.getUIContext().px2vp(1920))
  33. }
  34. }
  35. .size({ width: '100%', height: '100%' })
  36. .backgroundColor(Color.Black)
  37. }
  38. }

Obtaining the Device Fold State

You can use either of the following solutions.

  • Solution 1: Call CameraManager.on('foldStatusChange') provided by the camera framework to listen for fold state changes.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. import { camera } from '@kit.CameraKit';
    2. import { BusinessError } from '@kit.BasicServicesKit';
    3. function registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) {
    4. // The foldStatus variable is used to control the display of the XComponent.
    5. AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus);
    6. }
    7. function onFoldStatusChange(cameraManager: camera.CameraManager) {
    8. cameraManager.on('foldStatusChange', registerFoldStatusChanged);
    9. }
    10. function offFoldStatusChange(cameraManager: camera.CameraManager) {
    11. cameraManager.off('foldStatusChange', registerFoldStatusChanged);
    12. }
  • Solution 2: Call display.on('foldStatusChange') to listen for fold state changes.

    Collapse
    Word wrap
    Dark theme
    Copy code
    1. import { display } from '@kit.ArkUI';
    2. function getFoldStatus(): display.FoldStatus {
    3. let curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN;
    4. try {
    5. curFoldStatus = display.getFoldStatus();
    6. } catch (error) {
    7. console.error('getFoldStatus call failed');
    8. }
    9. return curFoldStatus;
    10. }
    11. let preFoldStatus: display.FoldStatus = getFoldStatus();
    12. display.on('foldStatusChange', (foldStatus: display.FoldStatus) => {
    13. // 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.
    14. if ((preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED &&
    15. foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) ||
    16. (preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED &&
    17. foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) {
    18. preFoldStatus = foldStatus;
    19. return;
    20. }
    21. preFoldStatus = foldStatus;
    22. // The foldStatus variable is used to control the display of the XComponent.
    23. AppStorage.setOrCreate<number>('foldStatus', foldStatus);
    24. })

Checking the Presence of a Camera at a Specific Position

You can call CameraManager.getSupportedCameras to obtain all the cameras supported by the device in the current fold state. By iterating through the results and using CameraPosition, you can determine whether a camera exists at the specified position.

Collapse
Word wrap
Dark theme
Copy code
  1. import { camera } from '@kit.CameraKit';
  2. // The default value of connectionType is camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN, indicating the device's built-in camera.
  3. function hasCameraAt(cameraManager: camera.CameraManager, cameraPosition: camera.CameraPosition,
  4. connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): boolean {
  5. let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras();
  6. if (cameraArray.length <= 0) {
  7. console.error('cameraManager.getSupportedCameras error');
  8. return false;
  9. }
  10. for (let index = 0; index < cameraArray.length; index++) {
  11. if (cameraArray[index].cameraPosition === cameraPosition &&
  12. cameraArray[index].connectionType === connectionType) {
  13. return true;
  14. }
  15. }
  16. return false;
  17. }

Camera Switching Logic

When a fold state change is detected, the foldStatus variable decorated by @StorageLink is updated, which triggers the reloadXComponent method to reload the XComponent, thereby implementing the camera switching logic.

Complete Sample Code

Collapse
Word wrap
Dark theme
Copy code
  1. import { camera } from '@kit.CameraKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { abilityAccessCtrl } from '@kit.AbilityKit';
  4. import { display } from '@kit.ArkUI';
  5. const TAG = 'FoldScreenCameraAdaptationDemo ';
  6. @Entry
  7. @Component
  8. struct Index {
  9. @State isShow: boolean = false;
  10. @State reloadXComponentFlag: boolean = false;
  11. @StorageLink('foldStatus') @Watch('reloadXComponent') foldStatus: number = 0;
  12. private mXComponentController: XComponentController = new XComponentController();
  13. private mXComponentOptions: XComponentOptions = {
  14. type: XComponentType.SURFACE,
  15. controller: this.mXComponentController
  16. }
  17. private mSurfaceId: string = '';
  18. private mCameraPosition: camera.CameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK;
  19. private mCameraManager: camera.CameraManager | undefined = undefined;
  20. // Select the surface width and height as required.
  21. private surfaceRect: SurfaceRect = {
  22. surfaceWidth: 1080,
  23. surfaceHeight: 1920
  24. };
  25. private curCameraDevice: camera.CameraDevice | undefined = undefined;
  26. private mCameraInput: camera.CameraInput | undefined = undefined;
  27. private mPreviewOutput: camera.PreviewOutput | undefined = undefined;
  28. private mPhotoSession: camera.PhotoSession | undefined = undefined;
  29. // Choose the appropriate preview stream profile based on your specific needs. This example uses a resolution of 1080P with CameraFormat: 1003.
  30. private previewProfileObj: camera.Profile = {
  31. format: 1003,
  32. size: {
  33. width: 1920,
  34. height: 1080
  35. }
  36. };
  37. private mContext: Context | undefined = undefined;
  38. private preFoldStatus: display.FoldStatus = this.getFoldStatus();
  39. // Listen for the foldable screen status. You can use cameraManager.on(type: 'foldStatusChange', callback: AsyncCallback<FoldStatusInfo>): void;
  40. // or display.on(type: 'foldStatusChange', callback: Callback<FoldStatus>): void;.
  41. private foldStatusCallback =
  42. (err: BusinessError, info: camera.FoldStatusInfo): void => this.registerFoldStatusChanged(err, info);
  43. private displayFoldStatusCallback =
  44. (foldStatus: display.FoldStatus): void => this.onDisplayFoldStatusChange(foldStatus);
  45. getFoldStatus(): display.FoldStatus {
  46. let curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN;
  47. try {
  48. curFoldStatus = display.getFoldStatus();
  49. } catch (error) {
  50. console.info(`${TAG} getFoldStatus call failed, error: ${error.code}`);
  51. }
  52. return curFoldStatus;
  53. }
  54. registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) {
  55. if (err !== undefined && err.code !== 0) {
  56. console.info(`${TAG} registerFoldStatusChanged call failed, error: ${err.code}`);
  57. return;
  58. }
  59. console.info(TAG + 'foldStatusChanged foldStatus: ' + foldStatusInfo.foldStatus);
  60. for (let i = 0; i < foldStatusInfo.supportedCameras.length; i++) {
  61. console.info(TAG +
  62. `foldStatusChanged camera[${i}]: ${foldStatusInfo.supportedCameras[i].cameraId},cameraPosition: ${foldStatusInfo.supportedCameras[i].cameraPosition}`);
  63. }
  64. AppStorage.setOrCreate<number>('foldStatus', foldStatusInfo.foldStatus);
  65. }
  66. onDisplayFoldStatusChange(foldStatus: display.FoldStatus): void {
  67. console.info(TAG + `onDisplayFoldStatusChange foldStatus: ${foldStatus}`);
  68. if ((this.preFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED &&
  69. foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED) ||
  70. (this.preFoldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED &&
  71. foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED)) {
  72. this.preFoldStatus = foldStatus;
  73. return;
  74. }
  75. this.preFoldStatus = foldStatus;
  76. if (!this.curCameraDevice) {
  77. return;
  78. }
  79. // The foldStatus variable is used to control the display of the XComponent.
  80. AppStorage.setOrCreate<number>('foldStatus', foldStatus);
  81. }
  82. requestPermissionsFn(): void {
  83. let atManager = abilityAccessCtrl.createAtManager();
  84. atManager.requestPermissionsFromUser(this.mContext, [
  85. 'ohos.permission.CAMERA'
  86. ]).then((): void => {
  87. this.isShow = true;
  88. }).catch((error: BusinessError): void => {
  89. console.error(`${TAG} requestPermissionsFromUser call failed, error: ${error.code}`);
  90. });
  91. }
  92. initContext(): void {
  93. let uiContext = this.getUIContext();
  94. this.mContext = uiContext.getHostContext();
  95. }
  96. initCameraManager(): void {
  97. try {
  98. this.mCameraManager = camera.getCameraManager(this.mContext);
  99. } catch (error) {
  100. console.error(`${TAG} getCameraManager call failed, error: ${error.code}`);
  101. }
  102. }
  103. aboutToAppear(): void {
  104. console.info(TAG + 'aboutToAppear is called');
  105. this.initContext();
  106. this.initCameraManager();
  107. this.requestPermissionsFn();
  108. this.onFoldStatusChange();
  109. }
  110. async aboutToDisappear(): Promise<void> {
  111. await this.releaseCamera();
  112. // Stop the listening.
  113. this.offFoldStatusChange();
  114. }
  115. async onPageShow(): Promise<void> {
  116. await this.initCamera(this.mSurfaceId, this.mCameraPosition);
  117. }
  118. async releaseCamera(): Promise<void> {
  119. // Stop the session.
  120. try {
  121. await this.mPhotoSession?.stop();
  122. } catch (error) {
  123. let err = error as BusinessError;
  124. console.error(TAG + 'Failed to stop session, errorCode = ' + err.code);
  125. }
  126. // Release the camera input stream.
  127. try {
  128. await this.mCameraInput?.close();
  129. } catch (error) {
  130. let err = error as BusinessError;
  131. console.error(TAG + 'Failed to close device, errorCode = ' + err.code);
  132. }
  133. // Release the preview output stream.
  134. try {
  135. await this.mPreviewOutput?.release();
  136. } catch (error) {
  137. let err = error as BusinessError;
  138. console.error(TAG + 'Failed to release previewOutput, errorCode = ' + err.code);
  139. }
  140. this.mPreviewOutput = undefined;
  141. // Release the session.
  142. try {
  143. await this.mPhotoSession?.release();
  144. } catch (error) {
  145. let err = error as BusinessError;
  146. console.error(TAG + 'Failed to release photoSession, errorCode = ' + err.code);
  147. }
  148. // Set the session to null.
  149. this.mPhotoSession = undefined;
  150. }
  151. onFoldStatusChange(): void {
  152. this.mCameraManager?.on('foldStatusChange', this.foldStatusCallback);
  153. // display.on('foldStatusChange', this.displayFoldStatusCallback);
  154. }
  155. offFoldStatusChange(): void {
  156. this.mCameraManager?.off('foldStatusChange', this.foldStatusCallback);
  157. // display.off('foldStatusChange', this.displayFoldStatusCallback);
  158. }
  159. reloadXComponent(): void {
  160. this.reloadXComponentFlag = !this.reloadXComponentFlag;
  161. }
  162. async loadXComponent(): Promise<void> {
  163. if (!this.mXComponentController) {
  164. console.error(TAG + 'mXComponentController is null');
  165. return;
  166. }
  167. this.mSurfaceId = this.mXComponentController.getXComponentSurfaceId();
  168. this.mXComponentController.setXComponentSurfaceRect(this.surfaceRect);
  169. console.info(TAG + `mCameraPosition: ${this.mCameraPosition}`)
  170. await this.initCamera(this.mSurfaceId, this.mCameraPosition);
  171. }
  172. getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined {
  173. let previewProfiles = cameraOutputCapability.previewProfiles;
  174. if (previewProfiles.length < 1) {
  175. return undefined;
  176. }
  177. let index = previewProfiles.findIndex((previewProfile: camera.Profile) => {
  178. return previewProfile.size.width === this.previewProfileObj.size.width &&
  179. previewProfile.size.height === this.previewProfileObj.size.height &&
  180. previewProfile.format === this.previewProfileObj.format;
  181. })
  182. if (index === -1) {
  183. return undefined;
  184. }
  185. return previewProfiles[index];
  186. }
  187. async initCamera(surfaceId: string, cameraPosition: camera.CameraPosition,
  188. connectionType: camera.ConnectionType = camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN): Promise<void> {
  189. await this.releaseCamera();
  190. // Create a CameraManager object.
  191. if (!this.mCameraManager) {
  192. console.error(TAG + 'camera.getCameraManager error');
  193. return;
  194. }
  195. // Obtain the camera list.
  196. let cameraArray: Array<camera.CameraDevice> = this.mCameraManager.getSupportedCameras();
  197. if (!cameraArray || cameraArray.length == 0) {
  198. console.error(TAG + 'cameraManager.getSupportedCameras error');
  199. return;
  200. }
  201. for (let index = 0; index < cameraArray.length; index++) {
  202. console.info(TAG + 'cameraId : ' + cameraArray[index].cameraId); // Obtain the camera ID.
  203. console.info(TAG + 'cameraPosition : ' + cameraArray[index].cameraPosition); // Obtain the camera position.
  204. console.info(TAG + 'cameraType : ' + cameraArray[index].cameraType); // Obtain the camera type.
  205. console.info(TAG + 'connectionType : ' + cameraArray[index].connectionType); // Obtain the camera connection type.
  206. }
  207. let deviceIndex = cameraArray.findIndex((cameraDevice: camera.CameraDevice) => {
  208. return cameraDevice.cameraPosition === cameraPosition && cameraDevice.connectionType === connectionType;
  209. })
  210. // If no camera is found at the specified position, you can select another camera. Handle the situation based on the specific scenario.
  211. if (deviceIndex === -1) {
  212. deviceIndex = 0;
  213. console.error(TAG + 'not found camera');
  214. }
  215. this.curCameraDevice = cameraArray[deviceIndex];
  216. // Create a camera input stream.
  217. try {
  218. this.mCameraInput = this.mCameraManager.createCameraInput(this.curCameraDevice);
  219. } catch (error) {
  220. let err = error as BusinessError;
  221. console.error(TAG + 'Failed to createCameraInput errorCode = ' + err.code);
  222. }
  223. if (this.mCameraInput === undefined) {
  224. return;
  225. }
  226. // Open the camera.
  227. try {
  228. await this.mCameraInput.open();
  229. } catch (error) {
  230. let err = error as BusinessError;
  231. console.error(TAG + 'Failed to open device, errorCode = ' + err.code);
  232. }
  233. // Obtain the supported modes.
  234. let sceneModes: Array<camera.SceneMode> = this.mCameraManager.getSupportedSceneModes(this.curCameraDevice);
  235. let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0;
  236. if (!isSupportPhotoMode) {
  237. console.error(TAG + 'photo mode not support');
  238. await this.releaseCamera();
  239. return;
  240. }
  241. // Obtain the output stream capability supported by the camera.
  242. let cameraOutputCapability: camera.CameraOutputCapability =
  243. this.mCameraManager.getSupportedOutputCapability(this.curCameraDevice, camera.SceneMode.NORMAL_PHOTO);
  244. if (!cameraOutputCapability) {
  245. console.error(TAG + 'cameraManager.getSupportedOutputCapability error');
  246. return;
  247. }
  248. console.info(TAG + 'outputCapability: ' + JSON.stringify(cameraOutputCapability));
  249. let previewProfile = this.getPreviewProfile(cameraOutputCapability);
  250. if (!previewProfile) {
  251. console.error(TAG + 'The resolution of the current preview stream is not supported.');
  252. await this.releaseCamera();
  253. return;
  254. }
  255. this.previewProfileObj = previewProfile;
  256. // Create a preview output stream. For details about the surfaceId parameter, see the XComponent. The preview stream uses the surface provided by the XComponent.
  257. try {
  258. this.mPreviewOutput = this.mCameraManager.createPreviewOutput(this.previewProfileObj, surfaceId);
  259. } catch (error) {
  260. let err = error as BusinessError;
  261. console.error(TAG + `Failed to create the PreviewOutput instance. error code: ${err.code}`);
  262. }
  263. if (!this.mPreviewOutput) {
  264. await this.releaseCamera();
  265. return;
  266. }
  267. // Create a session.
  268. try {
  269. let session = this.mCameraManager.createSession(camera.SceneMode.NORMAL_PHOTO);
  270. if (!session) {
  271. await this.releaseCamera();
  272. return;
  273. }
  274. this.mPhotoSession = session as camera.PhotoSession;
  275. } catch (error) {
  276. let err = error as BusinessError;
  277. console.error(TAG + 'Failed to create the session instance. errorCode = ' + err.code);
  278. }
  279. if (!this.mPhotoSession) {
  280. await this.releaseCamera();
  281. return;
  282. }
  283. // Start configuration for the session.
  284. try {
  285. this.mPhotoSession.beginConfig();
  286. } catch (error) {
  287. let err = error as BusinessError;
  288. console.error(TAG + 'Failed to beginConfig. errorCode = ' + err.code);
  289. }
  290. // Add the camera input stream to the session.
  291. try {
  292. this.mPhotoSession.addInput(this.mCameraInput);
  293. } catch (error) {
  294. let err = error as BusinessError;
  295. console.error(TAG + 'Failed to addInput. errorCode = ' + err.code);
  296. }
  297. // Add the preview output stream to the session.
  298. try {
  299. this.mPhotoSession.addOutput(this.mPreviewOutput);
  300. } catch (error) {
  301. let err = error as BusinessError;
  302. console.error(TAG + 'Failed to addOutput(previewOutput). errorCode = ' + err.code);
  303. }
  304. // Commit the session configuration.
  305. try {
  306. await this.mPhotoSession.commitConfig();
  307. } catch (error) {
  308. let err = error as BusinessError;
  309. console.error(TAG + 'Failed to commit session configuration, errorCode = ' + err.code);
  310. }
  311. // Start the session.
  312. try {
  313. await this.mPhotoSession.start()
  314. } catch (error) {
  315. let err = error as BusinessError;
  316. console.error(TAG + 'Failed to start session. errorCode = ' + err.code);
  317. }
  318. }
  319. build() {
  320. if (this.isShow) {
  321. Stack() {
  322. if (this.reloadXComponentFlag) {
  323. XComponent(this.mXComponentOptions)
  324. .onLoad(async () => {
  325. await this.loadXComponent();
  326. })
  327. .width(this.getUIContext().px2vp(1080))
  328. .height(this.getUIContext().px2vp(1920))
  329. } else {
  330. XComponent(this.mXComponentOptions)
  331. .onLoad(async () => {
  332. await this.loadXComponent();
  333. })
  334. .width(this.getUIContext().px2vp(1080))
  335. .height(this.getUIContext().px2vp(1920))
  336. }
  337. Text('Switch camera')
  338. .size({ width: 80, height: 48 })
  339. .position({ x: 1, y: 1 })
  340. .backgroundColor(Color.White)
  341. .textAlign(TextAlign.Center)
  342. .borderRadius(24)
  343. .onClick(async () => {
  344. this.mCameraPosition = this.mCameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK ?
  345. camera.CameraPosition.CAMERA_POSITION_FRONT : camera.CameraPosition.CAMERA_POSITION_BACK;
  346. this.reloadXComponentFlag = !this.reloadXComponentFlag;
  347. })
  348. }
  349. .size({ width: '100%', height: '100%' })
  350. .backgroundColor(Color.Black)
  351. }
  352. }
  353. }

FAQs

How to Handle Image Stretching or Distortion When the Fold State Changes

After a camera switch triggered by a fold state change, the image resolution may not match the aspect ratio of the XComponent. You can listen for window changes through Window.on('windowSizeChange') and determine the layout aspect ratio based on the relationship between the screen rotation angle (Display.rotation) and the camera orientation angle (CameraDevice.cameraOrientation), so that the XComponent aspect ratio adjusts in real time with the window.

For details about the code, see Specifying the Size of XComponent to Prevent Image Distortion After Rotation.

Search in Guides
Enter a keyword.