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