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