相机硬件差异
======

#### 概述

在移动端应用开发中，相机页面的多设备适配一直是开发者面临的一大难题。由于不同设备的屏幕尺寸、相机镜头、折叠形态以及系统特性等方面存在较大差异，相机界面的开发往往会遇到一系列[兼容性问题](#section1684283074912)，影响用户体验。

本文介绍如何将手机相机页面（含预览、拍摄和查看照片功能）适配至双折叠、Pura X、三折叠和平板等多种设备形态。在基础相机功能（预览、拍照、查看照片）之上，适配折叠屏和平板设备时，需要重点关注以下核心问题：

*   [通过断点实现多套页面布局](#section181143569262)，并设置横竖屏旋转策略。
*   [选择相机设备](#section13854163154917)。
*   [设置多设备上相机预览画面比例](#section882216138497)。
*   [设置拍照旋转角度](#section0752024124911)。
*   [实现悬停态相机页面](#section50639679)。

#### 通过断点实现多套页面布局

“两个宽度相近的窗口，页面布局应相同”。首先，根据这条原则，确认要适配窗口的宽度范围，手机、双折叠、Pura X、三折叠、平板设备共涉及到3种横向断点：sm、md和lg，因此应用首次开发时需要单独设计3种页面布局。

“对于高度相对宽度较小的窗口，呈现横向窗口或类方形窗口时，页面布局需进行差异化设计”。其次根据这条规则，因为Pura X外屏独特的小方形窗口形态，布局会与手机略有差异。UX设计图如下：

<table><tbody><tr><td class="cellrowborder" valign="top" width="6.756756756756757%"><p>断点</p></td><td class="cellrowborder" valign="top" width="13.166023166023166%"><p>横向断点sm，纵向断点md</p></td><td class="cellrowborder" valign="top" width="12.915057915057915%"><p>横向断点sm，纵向断点lg</p></td><td class="cellrowborder" valign="top" width="24.150579150579148%"><p>横向断点md</p></td><td class="cellrowborder" valign="top" width="43.011583011583014%"><p>横向断点lg</p></td></tr><tr><td class="cellrowborder" valign="top" width="6.756756756756757%"><p>UX图</p></td><td class="cellrowborder" valign="top" width="13.166023166023166%"><p><span><img height="164.3348" originheight="2475" originwidth="2003" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/9c/v3/gMzOT3BkSyWA-ywN8fR07Q/zh-cn_image_0000002355147085.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=25C1871BA6FCAD65F0FDE02537B121EF7E4A46727E0AFB3DD698C169DD8DB325" title="点击放大" width="133"></span></p></td><td class="cellrowborder" valign="top" width="12.915057915057915%"><p><span><img height="281.9334" originheight="2317" originwidth="1093" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/b8/v3/hJnMbiZcQRqKkuRO36g9aQ/zh-cn_image_0000002321148362.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=B972624F59F54FC741D716DBFE1D204AE41ECD1D80110355055EB1CB85266A65" title="点击放大" width="133"></span></p></td><td class="cellrowborder" valign="top" width="24.150579150579148%"><p><span><img height="296.4437" originheight="2425" originwidth="2176" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/9f/v3/jiuUlEPxSe2tLkn9PwhoTg/zh-cn_image_0000002355266917.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=B76301D2BDA5E288543EDC45FA8AC18DE6A8CBAE98C65F72F1406B17E8A83863" title="点击放大" width="266"></span></p></td><td class="cellrowborder" valign="top" width="43.011583011583014%"><p><span><img height="309.5043" originheight="2278" originwidth="3524" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/06/v3/5nIG58TmQWGC_dvpqNfWSw/zh-cn_image_0000002321308206.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=6F4CD9536592A6F5976B1D375F2A68F4E58FAA31C2D522E28CC524C793FF67B5" title="点击放大" width="478.8"></span></p></td></tr></tbody></table>

#### \[h2\]开发步骤

1.  分析三种横向断点对应的UX设计。sm断点的相机按钮布局分布在上下两侧，需要单独实现，Pura X外屏与手机布局的差异通过纵向断点进行区分；md、lg断点的相机按钮布局相同，分布在左右两侧，可以使用一套代码实现。使用Stack组件，展示相机预览画面上的控制按钮，并使用断点控制在三折叠不同使用状态下的显示与隐藏。相机页面中XComponent组件宽高均可设置为100%，预览画面的大小可通过设置XComponent组件对应Surface区域的宽高实现，代码可参考[设置多设备上相机预览画面比例](#section882216138497)。
    
    Stack() {
      // Camera View.
      Column() {
        XComponent({
          type: XComponentType.SURFACE,
          controller: this.xComponentController
        }) {}
        // ...
      }
      .width('100%')
      .height(this.isHalfFolded ? this.creaseRegion\[0\] : '')
      .layoutWeight(this.isHalfFolded ? 0 : 1)
      // ...
    
      // Shooting button view.
      Stack() {
        // Setting view for sm.
        Column() {
          // ...
        }
        .width(this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 30 : 48)
        .height('100%')
        .margin({ right: 16 })
        .padding({ top: this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 16 : 108 })
        .visibility(this.widthBp === WidthBreakpoint.WIDTH\_SM ? Visibility.Visible : Visibility.None)
    
        // Choose music for sm.
        Row() {
          // ...
        }
        .width('100%')
        .height(this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 28 : 40)
        .position({
          x: 0,
          y: this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 16 : 28
        })
        .justifyContent(FlexAlign.Center)
        .visibility(this.widthBp === WidthBreakpoint.WIDTH\_SM ? Visibility.Visible : Visibility.None)
    
        // Shooting button for sm.
        Column() {
          // ...
        }
        .visibility(this.widthBp === WidthBreakpoint.WIDTH\_SM ? Visibility.Visible : Visibility.None)
        .height(this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 96 : 132)
        .width('100%')
        .margin({ bottom: this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 16 : 84 })
    
        // Setting view for md/lg.
        Column() {
          // ...
        }
        .width(this.widthBp === WidthBreakpoint.WIDTH\_MD ? 144 : 152)
        .height('100%')
        .justifyContent(FlexAlign.Start)
        .padding({ left: this.heightBp === HeightBreakpoint.HEIGHT\_MD ? 24 : 32 })
        .position({ x: 0, y: 0 })
        .alignItems(HorizontalAlign.Start)
        .visibility(this.widthBp === WidthBreakpoint.WIDTH\_MD || this.widthBp === WidthBreakpoint.WIDTH\_LG ?
          Visibility.Visible : Visibility.None)
    
        // Shooting button for md/lg.
        Column() {
          // ...
        }
        .width(92)
        .height('100%')
        .justifyContent(FlexAlign.Center)
        .padding({ right: 16 })
        .visibility(this.widthBp === WidthBreakpoint.WIDTH\_MD || this.widthBp === WidthBreakpoint.WIDTH\_LG ?
          Visibility.Visible : Visibility.None)
      }
      .height('100%')
      .width('100%')
      .alignContent(Alignment.BottomEnd)
      .visibility(this.isHalfFolded ? Visibility.None : Visibility.Visible)
      // ...
    }
    .height('100%')
    .width('100%')
    .alignContent(this.widthBp === WidthBreakpoint.WIDTH\_MD ? (this.isHalfFolded ? Alignment.Top : Alignment.Start) :
      Alignment.Center)
    
2.  相机页面在不同设备上横竖屏旋转，请参考[为多设备配置旋转策略](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-multi-device-window-direction#section12636154743220)。以本应用为例，横向断点为sm时不支持旋转；md、lg时支持旋转。因此，当窗口宽度、高度的最小值大于等于600vp时，窗口支持旋转。
    
    onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => {
      this.setOrientation(this.uiContext!.px2vp(windowSize.width), this.uiContext!.px2vp(windowSize.height));
      // ...
    }
    
    setOrientation(width: number, height: number): void {
      // When the minimum value of window width and height is greater than the md breakpoint threshold, rotation is supported.
      if (Math.min(width, height) >= 600) {
        this.windowData?.setPreferredOrientation(window.Orientation.AUTO\_ROTATION\_RESTRICTED).catch((error: BusinessError) => {
          hilog.error(0x0000, \`MultiDeviceCamera\`,
            \`Set window orientation failed. Code: ${error.code}, message: ${error.message}\`);
        });
      } else {
        this.windowData?.setPreferredOrientation(window.Orientation.PORTRAIT).catch((error: BusinessError) => {
          hilog.error(0x0000, \`MultiDeviceCamera\`,
            \`Set window orientation failed. Code: ${error.code}, message: ${error.message}\`);
        });
      }
    }
    
    // ...
    onWindowStageCreate(windowStage: window.WindowStage): void {
      // ...
      windowStage.loadContent('pages/Index', (err) => {
        // ...
        windowStage.getMainWindow().then((data: window.Window) => {
          // ...
          // Monitor window size changes and update breakpoints.
          data.on('windowSizeChange', this.onWindowSizeChange);
          let rect: window.Rect = data.getWindowProperties().windowRect;
          this.setOrientation(this.uiContext.px2vp(rect.width), this.uiContext.px2vp(rect.height));
        }).catch((err: BusinessError) => {
          hilog.error(0x0000, 'testTag', \`Error occured, error code: ${err.code}, error message: ${err.message}\`);
        })
      });
    }
    

#### 选择相机设备

完整的相机页面，除了实现页面的按钮组件，还需要实现相机预览画面。在创建相机预览对象之前，首先要使用[createCameraInput()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-cameramanager#createcamerainput)确认使用的相机设备。

#### \[h2\]开发步骤

1.  手机、双折叠、Pura X、三折叠、平板均配有后置相机和前置相机。相机页面显示时，根据isFront变量选择对应CameraPosition的相机，默认使用后置相机。
    
    onPageShow(): void {
      abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, this.permissions).then(() => {
        setTimeout(() => {
          // After obtaining permission, load the camera preview stream and ensure it is consistent with the aspect ratio of the surface.
          // ...
          if (this.isFront) {
            this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_FRONT);
          } else {
            this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_BACK);
          }
        }, 200);
      }).catch((err: BusinessError) => {
        hilog.error(0x0000, 'testTag', \`Failed to requestPermissionsFromUser. Code: ${err.code}, message: ${err.message}\`);
      })
    }
    
    CameraPosition是相对的而不是固定的，例如Pura X外屏的相机。
    
    *   在折叠态时CameraPosition为CAMERA\_POSITION\_FRONT，效果图如下：
        
        ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/da/v3/VqxJwyD4RSOTHdoYu-R6dw/zh-cn_image_0000002355147089.png?HW-CC-KV=V1&HW-CC-Date=20260612T074612Z&HW-CC-Expire=86400&HW-CC-Sign=D3624454BF4C40D9FF92CDF0C235EFF876BE6B13132264A26BFEF72FF3B3C529 "点击放大")
        
    *   在展开态时CameraPosition为CAMERA\_POSITION\_BACK，效果图如下：
        
        ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/90/v3/PxuVdXTYT5GJj0CO7_72Rg/zh-cn_image_0000002321148366.png?HW-CC-KV=V1&HW-CC-Date=20260612T074612Z&HW-CC-Expire=86400&HW-CC-Sign=D70049C4572DC02CE9939F465D15A56B9F16656F893121AD830D1337FF189DCB "点击放大")
        
2.  其他需要重置预览流的场景需要开发者单独处理。折叠状态切换（例如双折叠的折叠态切换至半折叠态），会导致显示屏幕变化，需要重新选择相机设备。所以在[display.on('foldStatusChange')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-display#displayonfoldstatuschange10)中判断变化前后的折叠状态，并根据变化前使用的相机位置，选择变化后使用前置相机或后置相机。
    
    onFoldStatusChange: (foldStatus: display.FoldStatus) => void = (foldStatus: display.FoldStatus) => {
      if (foldStatus === display.FoldStatus.FOLD\_STATUS\_HALF\_FOLDED) {
        let orientation: display.Orientation = display.getDefaultDisplaySync().orientation;
        // Determine the page layout that has entered half folded status and prohibit portrait orientation.
        if (this.widthBp === WidthBreakpoint.WIDTH\_MD && (orientation === display.Orientation.LANDSCAPE ||
          orientation === display.Orientation.LANDSCAPE\_INVERTED)) {
          this.isHalfFolded = true;
          this.windowUtil.setMainWindowOrientation(window.Orientation.LANDSCAPE);
          this.cameraUtil.setHalfFoldedRect(this.windowUtil.getWindowSize());
        } else {
          if (this.oldFoldStatus === display.FoldStatus.FOLD\_STATUS\_FOLDED) {
            if (this.isFront) {
              this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_FRONT);
            } else {
              this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_BACK);
            }
          }
        }
        return;
      }
      // ...
      // Exit the half folded status page.
      if (this.isHalfFolded) {
        this.isHalfFolded = false;
        this.cameraUtil.setXComponentRect(this.windowUtil.getWindowSize());
      } else {
        if (foldStatus === display.FoldStatus.FOLD\_STATUS\_FOLDED ||
          (this.oldFoldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED\_WITH\_SECOND\_HALF\_FOLDED &&
            foldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED) ||
          (this.oldFoldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED &&
            foldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED\_WITH\_SECOND\_HALF\_FOLDED)) {
          if (this.isFront) {
            this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_FRONT);
          } else {
            this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_BACK);
          }
        }
      }
      // ...
    }
    
    aboutToAppear(): void {
      try {
        display.on('foldStatusChange', this.onFoldStatusChange);
      } catch (error) {
        let err = error as BusinessError;
        hilog.error(0x0000, 'MultiDeviceCamera', \`Failed to obtain fold status. Code: ${err.code}, message: ${err.message}\`);
      }
      // ...
    }
    
    需要注意，PuraX外屏只存在前置相机。如果在内屏使用的是后置相机，切换外屏后，后置相机将不再可用，则返回可用相机列表中默认的相机。
    
    getCamera(cameras: Array<camera.CameraDevice>, cameraPosition: camera.CameraPosition): number {
      // Choose front or rear camera.
      for (let i: number = 0; i < cameras.length; ++i) {
        if (cameras\[i\].cameraPosition === cameraPosition) {
          if (cameraPosition === camera.CameraPosition.CAMERA\_POSITION\_BACK) {
            AppStorage.setOrCreate('isFront', false);
          }
          if (cameraPosition === camera.CameraPosition.CAMERA\_POSITION\_FRONT) {
            AppStorage.setOrCreate('isFront', true);
          }
          return i;
        }
      }
      hilog.error(0x0000, 'testLog', \`Failed to find the camera with the corresponding position.\`);
      if (cameras\[0\].cameraPosition === camera.CameraPosition.CAMERA\_POSITION\_BACK) {
        AppStorage.setOrCreate('isFront', false);
      }
      if (cameras\[0\].cameraPosition === camera.CameraPosition.CAMERA\_POSITION\_FRONT) {
        AppStorage.setOrCreate('isFront', true);
      }
      return 0;
    }
    

#### 设置多设备上相机预览画面比例

选择相机之后，需要通过[createPreviewOutput()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-cameramanager#createpreviewoutput12)创建预览输出对象，绑定至XComponent组件展示预览画面，实现流程可参考[拍照实践](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-shooting-case)。在开发多设备上相机预览画面时，需要通过以下步骤避免压缩、拉伸、异常旋转的问题。

XComponent组件对应Surface区域的宽高比，取决于用户预览时设备的屏幕顺时针旋转角度。如果display.rotation为0°或180°，则Surface与相机预览流宽高比互为倒数；如果display.rotation为90°或270°，则Surface与相机预览流宽高比一致。

以获取预览流宽高比4:3为例（Pura X折叠态为1:1），多设备不同状态的Surface宽高比如下表：

<table><tbody><tr><td class="cellrowborder" valign="top" width="9.27%">&nbsp;&nbsp;</td><td class="cellrowborder" valign="top" width="16.259999999999998%"><p>Pura X折叠态</p></td><td class="cellrowborder" valign="top" width="19.18%"><p>手机/双折叠折叠态/三折叠F态/Pura X展开态</p></td><td class="cellrowborder" valign="top" width="17.630000000000003%"><p>双折叠展开态/三折叠M态</p></td><td class="cellrowborder" valign="top" width="17.21%"><p>三折叠G态</p></td><td class="cellrowborder" valign="top" width="20.45%"><p>平板</p></td></tr><tr><td class="cellrowborder" valign="top" width="9.27%"><p>设备方向</p></td><td class="cellrowborder" valign="top" width="16.259999999999998%"><p>以充电口朝右为例</p><p>display.rotation=270</p></td><td class="cellrowborder" valign="top" width="19.18%"><p>以充电口朝下为例</p><p>display.rotation=0</p></td><td class="cellrowborder" valign="top" width="17.630000000000003%"><p>以充电口朝下为例</p><p>display.rotation=0</p></td><td class="cellrowborder" valign="top" width="17.21%"><p>以充电口朝下为例</p><p>display.rotation=0</p></td><td class="cellrowborder" valign="top" width="20.45%"><p>以充电口朝下为例</p><p>display.rotation=0</p></td></tr><tr><td class="cellrowborder" valign="top" width="9.27%"><p>Surface宽高比</p></td><td class="cellrowborder" valign="top" width="16.259999999999998%"><p>1:1</p></td><td class="cellrowborder" valign="top" width="19.18%"><p>3:4</p></td><td class="cellrowborder" valign="top" width="17.630000000000003%"><p>3:4</p></td><td class="cellrowborder" valign="top" width="17.21%"><p>3:4</p></td><td class="cellrowborder" valign="top" width="20.45%"><p>3:4</p></td></tr><tr><td class="cellrowborder" valign="top" width="9.27%"><p>设备方向</p></td><td class="cellrowborder" rowspan="2" valign="top" width="16.259999999999998%"><p>页面不支持旋转</p></td><td class="cellrowborder" rowspan="2" valign="top" width="19.18%"><p>页面不支持旋转</p></td><td class="cellrowborder" valign="top" width="17.630000000000003%"><p>以充电口朝右为例</p><p>display.rotation=270</p></td><td class="cellrowborder" valign="top" width="17.21%"><p>以充电口朝右为例</p><p>display.rotation=270</p></td><td class="cellrowborder" valign="top" width="20.45%"><p>以充电口朝右为例</p><p>display.rotation=270</p></td></tr><tr><td class="cellrowborder" valign="top"><p>Surface宽高比</p></td><td class="cellrowborder" valign="top"><p>4:3</p></td><td class="cellrowborder" valign="top"><p>4:3</p></td><td class="cellrowborder" valign="top"><p>4:3</p></td></tr></tbody></table>

多设备不同状态的预览效果图如下表：

<table><tbody><tr><td class="cellrowborder" valign="top" width="8.94910508949105%"><p>Pura X折叠态</p></td><td class="cellrowborder" valign="top" width="15.238476152384761%"><p>手机/双折叠折叠态/三折叠F态/Pura X展开态</p></td><td class="cellrowborder" valign="top" width="21.747825217478255%"><p>双折叠展开态/三折叠M态</p></td><td class="cellrowborder" valign="top" width="24.857514248575143%"><p>三折叠G态</p></td><td class="cellrowborder" valign="top" width="29.207079292070798%"><p>平板</p></td></tr><tr><td class="cellrowborder" valign="top" width="8.94910508949105%"><p><span><img height="164.3348" originheight="2475" originwidth="2003" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/82/v3/1cYTTwqlQUGU7vrkmCwPcA/zh-cn_image_0000002355266921.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=4E9B686768989682F1E1E243C0CABC1B11BDCAF79976272CBE39BBC9F3D2FECC" title="点击放大" width="133"></span></p></td><td class="cellrowborder" valign="top" width="15.238476152384761%"><p><span><img height="281.9334" originheight="2317" originwidth="1093" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/9a/v3/mnkS5yWpT_u1XDmGKqqO4w/zh-cn_image_0000002321308218.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=4D9D96E53B496A36B48DEAEE502ED70F9DFD99C3DC72FA406F9B5B2851A3D8E0" title="点击放大" width="133"></span></p></td><td class="cellrowborder" valign="top" width="21.747825217478255%"><p><span><img height="296.4304" originheight="2425" originwidth="2176" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/81/v3/mADd0dTKQ-S64y89bW0lUA/zh-cn_image_0000002355147093.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=3716B490D74160F12AD16204648670AA58827478C15FDEA1BBC59142C9DE1FA1" title="点击放大" width="266"></span></p></td><td class="cellrowborder" valign="top" width="24.857514248575143%"><p><span><img height="283.60920000000004" originheight="2475" originwidth="3482" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/c3/v3/z7GqRbDBThqVgsVaG5D30Q/zh-cn_image_0000002321148390.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=46FB5CCFD94F556441A2E08626A8A3A3F2A44541F8477394A9E7F38FC92D9573" title="点击放大" width="399"></span></p></td><td class="cellrowborder" valign="top" width="29.207079292070798%"><p><span><img height="478.8" originheight="3524" originwidth="2278" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/be/v3/4nGkUz_xSFGvk_LFBSNnVw/zh-cn_image_0000002355266941.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=9B5F9118C0C691D2A63FE6F4A20CEF56AAD5B8559A7B9E43A474AC256521BE1D" title="点击放大" width="309.5176"></span></p></td></tr><tr><td class="cellrowborder" valign="top" width="8.94910508949105%"><p>页面不支持旋转</p></td><td class="cellrowborder" valign="top" width="15.238476152384761%"><p>页面不支持旋转</p></td><td class="cellrowborder" valign="top" width="21.747825217478255%"><p><span><img height="266" originheight="2176" originwidth="2425" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/35/v3/MigSX2KgQ_qWTYm81r7LdA/zh-cn_image_0000002321308238.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=CA586912B1E7B6404E9FDCF280FB471FACD5D1927227A30184E87E04A008D097" title="点击放大" width="296.4437"></span></p></td><td class="cellrowborder" valign="top" width="24.857514248575143%"><p><span><img height="399" originheight="3482" originwidth="2475" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/2f/v3/t8_yNvLkRk6udORgMoxDmQ/zh-cn_image_0000002355147121.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=78AECCDE88149B14E9892FA7E26C8AB6E86FAD3DC33D75E98B6AFC047A2E0CE1" title="点击放大" width="283.60920000000004"></span></p></td><td class="cellrowborder" valign="top" width="29.207079292070798%"><p><span><img height="309.5176" originheight="2278" originwidth="3524" src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/3/v3/iJu8OLMMT4K4cW9kfHw21g/zh-cn_image_0000002321148422.png?HW-CC-KV=V1&amp;HW-CC-Date=20260612T074612Z&amp;HW-CC-Expire=86400&amp;HW-CC-Sign=184EEAC749D465B9E54E6D696E0B16F3B99ADFF9AF17171EF36A3B90952B39B2" title="点击放大" width="478.8"></span></p></td></tr></tbody></table>

#### \[h2\]开发步骤

1.  在窗口横竖屏旋转时，实现[setXComponentSurfaceRect()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-xcomponent#setxcomponentsurfacerect12)方法重新设置XComponent组件中surface区域的宽和高，确保与多设备相机的预览流、拍照流宽高比一致。
    
    setXComponentRect(windowSize: window.Size): void {
      try {
        // Initialize the width and height of the surface to match the full screen of the window.
        let rect: SurfaceRect = {
          surfaceWidth: windowSize.width,
          surfaceHeight: windowSize.height
        };
        let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint();
        let heightBp: HeightBreakpoint = this.uiContext!.getWindowHeightBreakpoint();
        let displayRotation: number = display.getDefaultDisplaySync().rotation \* 90;
        if (widthBp === WidthBreakpoint.WIDTH\_SM && heightBp === HeightBreakpoint.HEIGHT\_MD) {
          this.xComponentController!.setXComponentSurfaceRect(rect);
          return;
        }
        if (AppStorage.get('isHalfFolded')) {
          this.setHalfFoldedRect(windowSize);
          return;
        }
        if (displayRotation === 0 || displayRotation === 180) {
          if (windowSize.height \* 3 / 4 > windowSize.width) {
            rect.surfaceHeight = windowSize.width / 3 \* 4;
          } else {
            rect.surfaceWidth = windowSize.height / 4 \* 3;
          }
          if (widthBp === WidthBreakpoint.WIDTH\_MD && heightBp === HeightBreakpoint.HEIGHT\_MD) {
            rect.offsetX = 0;
            rect.offsetY = 0;
          }
        }
        if (displayRotation === 90 || displayRotation === 270) {
          if (windowSize.width \* 3 / 4 > windowSize.height) {
            rect.surfaceWidth = windowSize.height / 3 \* 4;
          } else {
            rect.surfaceHeight = windowSize.width / 4 \* 3;
          }
        }
        this.xComponentController!.setXComponentSurfaceRect(rect);
      } catch (error) {
        let err = error as BusinessError;
        hilog.error(0x0000, 'MultiDeviceCamera', \`Failed to set XComponent rect. Code: ${err.code}, message: ${err.message}\`);
      }
    }
    
2.  在窗口横竖屏旋转时，会刷新窗口尺寸，需要调用setXComponentRect()方法刷新预览画面比例，避免预览画面异常压缩或拉伸。
    
    export default class EntryAbility extends UIAbility {
      // ...
      isFirstTime: boolean = true;
      onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => {
        // ...
        if (!this.isFirstTime) {
          this.cameraUtil!.setXComponentRect(this.windowUtil!.getWindowSize());
        } else {
          this.isFirstTime = false;
        }
      }
      // ...
      onWindowStageCreate(windowStage: window.WindowStage): void {
        // ...
        windowStage.loadContent('pages/Index', (err) => {
          // ...
          windowStage.getMainWindow().then((data: window.Window) => {
            // ...
            // Monitor window size changes and update breakpoints.
            data.on('windowSizeChange', this.onWindowSizeChange);
            // ...
          }).catch((err: BusinessError) => {
            hilog.error(0x0000, 'testTag', \`Error occured, error code: ${err.code}, error message: ${err.message}\`);
          })
        });
      }
      // ...
    }
    
3.  最后调用[setXComponentSurfaceRotation()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-xcomponent#setxcomponentsurfacerotation12)设置XComponent的Surface区域在屏幕旋转时锁定方向，确保相机预览画面旋转时的用户体验。
    
    XComponent({
      type: XComponentType.SURFACE,
      controller: this.xComponentController
    }) {}
    .onLoad(() => {
      // ...
      // Set surface to lock direction when screen rotates.
      this.xComponentController.setXComponentSurfaceRotation({ lock: true });
    })
    

#### 设置拍照旋转角度

在横竖屏拍照场景下，需确保图片始终正向显示，避免出现照片方向异常（如旋转90°或倒置）。

#### \[h2\]开发步骤

在[capture()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-photooutput#capture-3)方法中，通过重力传感器获取当前拍照的角度，并区分后置相机与前置相机，设置拍照时的旋转角度rotation。

capture(): void {
  let rotation: number = 0;
  let isFront: boolean | undefined = AppStorage.get('isFront');
  try {
    // Obtain the angle of the gravity sensor during shooting and set the shooting rotation angle.
    sensor.once(sensor.SensorId.GRAVITY, (data: sensor.GravityResponse) => {
      if (Math.abs(data.z) > OVERLOOKING\_GRAVITY\_OF\_Z\_AXIS) {
        rotation = this.lastRotation;
      }
      else {
        let degree: number = this.getCalDegree(data.x, data.y, data.z);
        if ((degree >= 0 && degree <= 30) || degree >= 300) {
          rotation = camera.ImageRotation.ROTATION\_0;
        } else if (degree > 30 && degree <= 120) {
          if (isFront) {
            // Use ROTATION\_270 when degree range is (30, 120\] for front camera.
            rotation = camera.ImageRotation.ROTATION\_270;
          } else {
            // Use ROTATION\_90 when degree range is (30, 120\] for back camera.
            rotation = camera.ImageRotation.ROTATION\_90;
          }
        } else if (degree > 120 && degree <= 210) {
          // Use ROTATION\_180 when degree range is (120, 210\].
          rotation = camera.ImageRotation.ROTATION\_180;
        } else if (degree > 210 && degree <= 300) {
          if (isFront) {
            // Use ROTATION\_90 when degree range is (210, 300\] for front camera.
            rotation = camera.ImageRotation.ROTATION\_90;
          } else {
            // Use ROTATION\_270 when degree range is (210, 300\] for back camera.
            rotation = camera.ImageRotation.ROTATION\_270;
          }
        };
      }

      let setting: camera.PhotoCaptureSetting = {
        quality: camera.QualityLevel.QUALITY\_LEVEL\_HIGH,
        rotation: rotation,
        mirror: isFront
      }
      this.photoOutput?.capture(setting);
    })
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(0x0000, 'MultiDeviceCamera', \`Capture failed. Code: ${err.code}, message: ${err.message}\`);
  }
}

#### 实现悬停态相机页面

悬停态对应折叠状态为FOLD\_STATUS\_HALF\_FOLDED。在进入悬停态时，可以设计特殊的用户体验，UX效果图如下：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/7f/v3/tGA-S0BSSCWG6I4DS1jt0w/zh-cn_image_0000002355266965.png?HW-CC-KV=V1&HW-CC-Date=20260612T074612Z&HW-CC-Expire=86400&HW-CC-Sign=14FF81255466A250E685A8527EA205BAF049198AE6339E5B62382E71C5C415B3 "点击放大")

#### \[h2\]开发步骤

使用[display.on('foldStatusChange')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-display#displayonfoldstatuschange10)监听折叠状态的变化。当折叠状态为半折叠且屏幕显示方向为横屏或反向横屏时，记录悬停状态isHalfFolded为true，重新计算Xcomponent的Surface显示区域宽高并调整旋转策略，展示悬停态的相机页面布局；否则，记录悬停态状态为false。

onFoldStatusChange: (foldStatus: display.FoldStatus) => void = (foldStatus: display.FoldStatus) => {
  if (foldStatus === display.FoldStatus.FOLD\_STATUS\_HALF\_FOLDED) {
    let orientation: display.Orientation = display.getDefaultDisplaySync().orientation;
    // Determine the page layout that has entered half folded status and prohibit portrait orientation.
    if (this.widthBp === WidthBreakpoint.WIDTH\_MD && (orientation === display.Orientation.LANDSCAPE ||
      orientation === display.Orientation.LANDSCAPE\_INVERTED)) {
      this.isHalfFolded = true;
      this.windowUtil.setMainWindowOrientation(window.Orientation.LANDSCAPE);
      this.cameraUtil.setHalfFoldedRect(this.windowUtil.getWindowSize());
    } else {
      if (this.oldFoldStatus === display.FoldStatus.FOLD\_STATUS\_FOLDED) {
        if (this.isFront) {
          this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_FRONT);
        } else {
          this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_BACK);
        }
      }
    }
    return;
  }
  if (this.widthBp !== WidthBreakpoint.WIDTH\_SM) {
    this.windowUtil.setMainWindowOrientation(window.Orientation.AUTO\_ROTATION\_RESTRICTED);
  }
  // Exit the half folded status page.
  if (this.isHalfFolded) {
    this.isHalfFolded = false;
    this.cameraUtil.setXComponentRect(this.windowUtil.getWindowSize());
  } else {
    if (foldStatus === display.FoldStatus.FOLD\_STATUS\_FOLDED ||
      (this.oldFoldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED\_WITH\_SECOND\_HALF\_FOLDED &&
        foldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED) ||
      (this.oldFoldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED &&
        foldStatus === display.FoldStatus.FOLD\_STATUS\_EXPANDED\_WITH\_SECOND\_HALF\_FOLDED)) {
      if (this.isFront) {
        this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_FRONT);
      } else {
        this.cameraUtil.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA\_POSITION\_BACK);
      }
    }
  }
  this.oldFoldStatus = foldStatus;
}

#### 常见问题

#### \[h2\]按钮大小异常、被截断

**问题现象**

在不同设备上显示按钮大小异常或被截断。

**可能原因**

未设计不同设备对应的多套相机页面布局。

**解决方案**

针对不同横向断点设计多套相机页面布局并实现。详情请参考[通过断点实现多套页面布局](#section181143569262)。

#### \[h2\]大屏相机页面的可操作组件不支持旋转

**问题现象**

在三折叠G态、双折叠展开态、平板等大屏幕，相机页面的可操作组件不支持旋转，不易操作。

**可能原因**

未设置窗口支持旋转。

**解决方案**

相机页面的横向断点为sm时，不支持旋转；为md、lg时，支持旋转。因此当窗口宽度、高度的最小值大于等于600vp时，窗口支持旋转。详情请参考[通过断点实现多套页面布局](#section181143569262)。

#### \[h2\]预览画面压缩、拉伸

**问题现象**

预览画面的显示内容被压缩或拉伸。

**可能原因**

相机预览对象绑定XComponent组件时，未正确设置Surface区域宽高的值，导致宽高比与预览流旋转后的宽高比不一致。

**解决方案**

设置XComponent组件对应Surface区域的宽高比，使其与预览流旋转后的宽高比保持一致。详情请参考[设置多设备上相机预览画面比例](#section882216138497)。

#### \[h2\]拍完照片显示角度异常

**问题现象**

拍摄完成后，照片的实际内容与预期方向不符，可能发生90°、180°或270°的旋转，导致用户需要手动调整才能正常查看。

**可能原因**

拍照时旋转了设备，且未设置正确的拍照角度。

**解决方案**

设置拍照时的旋转角度。详情请参考[设置拍照旋转角度](#section0752024124911)。

#### \[h2\]折叠屏切换折叠状态时出现黑屏

**问题现象**

在折叠屏开合，切换折叠状态后，相机预览页面出现黑屏。

**可能原因**

切换折叠状态过程，导致折叠前选择的相机不再可用，预览画面黑屏。

**解决方案**

折叠状态切换过程窗口尺寸会变化，通过监听窗口尺寸变化，重新选择相机生成预览流。详情请参考[选择相机设备](#section13854163154917)。

#### 示例代码

*   [基于相机开放能力和一多能力实现多设备相机](https://gitcode.com/harmonyos_samples/MultiDeviceCamera)