管理中心

适配不同折叠状态的摄像头变更(ArkTS)

折叠设备形态各异,在相机应用的开发过程中需要统一的摄像头切换方案,以确保用户在拍照、录像过程中获得更好的体验。

一台可折叠设备在不同折叠状态下,可使用不同的相机。系统会标识所有摄像头,每个摄像头与一个折叠状态相对应,表示该摄像头可在对应的折叠状态下使用。应用可调用CameraManager.on('foldStatusChange')或display.on('foldStatusChange')监听设备的折叠状态变化,并调用CameraManager.getSupportedCameras获取当前状态下可用相机,完成相应适配,确保应用在折叠状态变更时的用户体验。

不同折叠设备在不同折叠状态下支持的摄像头数量不同。

例如,折叠设备拥有三颗摄像头:后置摄像头A、前置摄像头B和前置摄像头C。在展开状态下,通过CameraManager.getSupportedCameras接口可获取到后置摄像头A和前置摄像头B;在折叠状态下,可获取到后置摄像头A和前置摄像头C。不同的折叠状态获取到的镜头不同,因此,在折叠状态变化时,需要重新获取镜头信息。

详细的API说明请参考@ohos.multimedia.camera (相机管理)。

Context获取方式请参考:获取UIAbility的上下文信息。

在开发相机应用时,需要先申请相机相关权限,请参考申请相关权限。

创建XComponent

使用两个XComponent分别展示折叠态和展开态,防止切换折叠屏状态亮屏的时候上一个相机还未关闭,残留上一个相机的画面。

收起
自动换行
深色代码主题
复制
  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. // 初始化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. }

获取设备折叠状态

此处提供两种方案供开发者选择。

  • 方案一:使用相机框架提供的CameraManager.on('foldStatusChange')监听设备折叠态变化。
    收起
    自动换行
    深色代码主题
    复制
    1. import { camera } from '@kit.CameraKit';
    2. import { BusinessError } from '@kit.BasicServicesKit';
    3. function registerFoldStatusChanged(err: BusinessError, foldStatusInfo: camera.FoldStatusInfo) {
    4. // foldStatus 变量用来控制显示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. }
  • 方案二:使用图形图像的display.on('foldStatusChange')监听设备折叠态变化。
    收起
    自动换行
    深色代码主题
    复制
    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. // 从半折叠态(FOLD_STATUS_HALF_FOLDED)到展开态(FOLD_STATUS_EXPANDED),相机框架返回所支持的相机是一致的,所以从半折叠态到展开态不需要重新配流,从展开态到半折叠态也是一样的。
    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. // foldStatus 变量用来控制显示XComponent组件。
    23. AppStorage.setOrCreate<number>('foldStatus', foldStatus);
    24. })

判断是否存在对应位置摄像头

通过CameraManager.getSupportedCameras接口可获取到当前设备折叠状态下支持的所有镜头,遍历获取到的结果,通过CameraPosition判断镜头是否存在。

收起
自动换行
深色代码主题
复制
  1. import { camera } from '@kit.CameraKit';
  2. // connectionType默认为camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN,表示设备的内置镜头。
  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. }

摄像头切换逻辑

在监听到折叠状态发生变化时通过设置被@StorageLink修饰的foldStatus变量改变,触发reloadXComponent方法重新加载XComponent组件,从而实现相机的切换逻辑。

完整示例

收起
自动换行
深色代码主题
复制
  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. // surface宽高根据需要自行选择。
  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. // 请根据实际业务诉求选择符合需求场景的预览流Profile,此处以分辨率1080P,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. // 监听折叠屏状态,可以使用cameraManager.on(type: 'foldStatusChange', callback: AsyncCallback<FoldStatusInfo>): void;
  40. // 也可以使用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. // foldStatus 变量用来控制显示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. // 解注册。
  113. this.offFoldStatusChange();
  114. }
  115. async onPageShow(): Promise<void> {
  116. await this.initCamera(this.mSurfaceId, this.mCameraPosition);
  117. }
  118. async releaseCamera(): Promise<void> {
  119. // 停止当前会话。
  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. // 释放相机输入流。
  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. // 释放预览输出流。
  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. // 释放会话。
  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. // 会话置空。
  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. // 创建CameraManager对象。
  191. if (!this.mCameraManager) {
  192. console.error(TAG + 'camera.getCameraManager error');
  193. return;
  194. }
  195. // 获取相机列表。
  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); // 获取相机ID。
  203. console.info(TAG + 'cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置。
  204. console.info(TAG + 'cameraType : ' + cameraArray[index].cameraType); // 获取相机类型。
  205. console.info(TAG + 'connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型。
  206. }
  207. let deviceIndex = cameraArray.findIndex((cameraDevice: camera.CameraDevice) => {
  208. return cameraDevice.cameraPosition === cameraPosition && cameraDevice.connectionType === connectionType;
  209. })
  210. // 没有找到对应位置的摄像头,可选择其他摄像头,具体场景具体对待。
  211. if (deviceIndex === -1) {
  212. deviceIndex = 0;
  213. console.error(TAG + 'not found camera');
  214. }
  215. this.curCameraDevice = cameraArray[deviceIndex];
  216. // 创建相机输入流。
  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. // 打开相机。
  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. // 获取支持的模式类型。
  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. // 获取相机设备支持的输出流能力。
  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. // 创建预览输出流,其中参数 surfaceId 参考上文 XComponent 组件,预览流为XComponent组件提供的surface。
  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. // 创建会话。
  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. // 开始配置会话。
  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. // 向会话中添加相机输入流。
  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. // 向会话中添加预览输出流。
  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. // 提交会话配置。
  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. // 启动会话。
  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('切换相机')
  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. }

常见问题

折叠状态改变时,如何处理相机画面可能出现的拉伸变形问题

折叠状态发生变化导致镜头切换后,可能出现图像分辨率与XComponent的宽高比不匹配的情况。应用可通过Window.on('windowSizeChange')监听窗口的变化,根据屏幕旋转角度(Display.rotation)与相机镜头角度(CameraDevice.cameraOrientation)之间的关系来确定布局的宽高比,以确保XComponent的宽高比能跟随窗口实时调整。

详细代码可参考:指定XComponent的大小,防止旋转后图像拉伸变形。