智能客服
你问我答,随时在线为你解决问题
在移动端应用开发中,相机页面的多设备适配一直是开发者面临的一大难题。由于不同设备的屏幕尺寸、相机镜头、折叠形态以及系统特性等方面存在较大差异,相机界面的开发往往会遇到一系列兼容性问题,影响用户体验。
本文介绍如何将手机相机页面(含预览、拍摄和查看照片功能)适配至双折叠、Pura X、三折叠和平板等多种设备形态。在基础相机功能(预览、拍照、查看照片)之上,适配折叠屏和平板设备时,需要重点关注以下核心问题:
“两个宽度相近的窗口,页面布局应相同”。首先,根据这条原则,确认要适配窗口的宽度范围,手机、双折叠、Pura X、三折叠、平板设备共涉及到3种横向断点:sm、md和lg,因此应用首次开发时需要单独设计3种页面布局。
断点 | 横向断点sm,纵向断点md | 横向断点sm,纵向断点lg | 横向断点md | 横向断点lg |
UX图 |
|
|
|
|
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)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()确认使用的相机设备。
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外屏的相机。


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()创建预览输出对象,绑定至XComponent组件展示预览画面,实现流程可参考拍照实践。在开发多设备上相机预览画面时,需要通过以下步骤避免压缩、拉伸、异常旋转的问题。
XComponent组件对应Surface区域的宽高比,取决于用户预览时设备的屏幕顺时针旋转角度。如果display.rotation为0°或180°,则Surface与相机预览流宽高比互为倒数;如果display.rotation为90°或270°,则Surface与相机预览流宽高比一致。
以获取预览流宽高比4:3为例(Pura X折叠态为1:1),多设备不同状态的Surface宽高比如下表:
Pura X折叠态 | 手机/双折叠折叠态/三折叠F态/Pura X展开态 | 双折叠展开态/三折叠M态 | 三折叠G态 | 平板 | |
设备方向 | 以充电口朝右为例 display.rotation=270 | 以充电口朝下为例 display.rotation=0 | 以充电口朝下为例 display.rotation=0 | 以充电口朝下为例 display.rotation=0 | 以充电口朝下为例 display.rotation=0 |
Surface宽高比 | 1:1 | 3:4 | 3:4 | 3:4 | 3:4 |
设备方向 | 页面不支持旋转 | 页面不支持旋转 | 以充电口朝右为例 display.rotation=270 | 以充电口朝右为例 display.rotation=270 | 以充电口朝右为例 display.rotation=270 |
Surface宽高比 | 4:3 | 4:3 | 4:3 |
多设备不同状态的预览效果图如下表:
Pura X折叠态 | 手机/双折叠折叠态/三折叠F态/Pura X展开态 | 双折叠展开态/三折叠M态 | 三折叠G态 | 平板 |
|
|
|
|
|
页面不支持旋转 | 页面不支持旋转 |
|
|
|
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}`);
}
}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}`);
})
});
}
// ...
}XComponent({
type: XComponentType.SURFACE,
controller: this.xComponentController
}) {}
.onLoad(() => {
// ...
// Set surface to lock direction when screen rotates.
this.xComponentController.setXComponentSurfaceRotation({ lock: true });
})在横竖屏拍照场景下,需确保图片始终正向显示,避免出现照片方向异常(如旋转90°或倒置)。
在capture()方法中,通过重力传感器获取当前拍照的角度,并区分后置相机与前置相机,设置拍照时的旋转角度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效果图如下:

使用display.on('foldStatusChange')监听折叠状态的变化。当折叠状态为半折叠且屏幕显示方向为横屏或反向横屏时,记录悬停状态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;
}问题现象
在不同设备上显示按钮大小异常或被截断。
可能原因
未设计不同设备对应的多套相机页面布局。
解决方案
针对不同横向断点设计多套相机页面布局并实现。详情请参考通过断点实现多套页面布局。
问题现象
在三折叠G态、双折叠展开态、平板等大屏幕,相机页面的可操作组件不支持旋转,不易操作。
可能原因
未设置窗口支持旋转。
解决方案
相机页面的横向断点为sm时,不支持旋转;为md、lg时,支持旋转。因此当窗口宽度、高度的最小值大于等于600vp时,窗口支持旋转。详情请参考通过断点实现多套页面布局。
问题现象
预览画面的显示内容被压缩或拉伸。
可能原因
相机预览对象绑定XComponent组件时,未正确设置Surface区域宽高的值,导致宽高比与预览流旋转后的宽高比不一致。
解决方案
设置XComponent组件对应Surface区域的宽高比,使其与预览流旋转后的宽高比保持一致。详情请参考设置多设备上相机预览画面比例。
问题现象
拍摄完成后,照片的实际内容与预期方向不符,可能发生90°、180°或270°的旋转,导致用户需要手动调整才能正常查看。
可能原因
拍照时旋转了设备,且未设置正确的拍照角度。
解决方案
设置拍照时的旋转角度。详情请参考设置拍照旋转角度。
问题现象
在折叠屏开合,切换折叠状态后,相机预览页面出现黑屏。
可能原因
切换折叠状态过程,导致折叠前选择的相机不再可用,预览画面黑屏。
解决方案
折叠状态切换过程窗口尺寸会变化,通过监听窗口尺寸变化,重新选择相机生成预览流。详情请参考选择相机设备。