往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)
✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✏️ 记录一场鸿蒙开发岗位面试经历~
✏️ 持续更新中……
概述
在一多开发过程中,开发者需要适配多种不同 窗口类型(WindowType) ,且同一窗口类型在不同设备上会有不同的属性(尺寸大小、系统区域、是否沉浸、自由窗口有标题栏等),针对窗口类型及属性的差异所产生的问题,本文将提供相应说明以及解决方案指导。包含以下常见场景:
- 应用窗口尺寸发生变化时页面如何更新断点从而刷新页面。
- 不同设备的横竖屏旋转采取怎样的策略以及实现方案。
- 应用窗口沉浸式页面如何实现。
- PC/2in1设备中使用自由窗如何适配窗口化、标题栏、全屏沉浸式。
窗口尺寸变化更新断点
应用页面展示时,要求窗口宽度在一定范围内,页面展示相同的页面布局。基于该原则,将窗口宽度划分为不同的范围(即断点),当窗口宽度从一个断点变化到另一个断点时,改变页面布局(如将页面内容从单列排布调整为双列排布甚至三列排布等)以获得更好的显示效果。
在实际开发过程中,仅靠窗口宽度计算的横向断点,无法区分所有窗口场景,比如手机切换横屏和折叠屏展开窗口时宽度都在600-840vp,即横向断点均为“md”,但两者的页面布局不同。因此,需要引入纵向断点并监听横纵向断点变化,实现区分多设备场景以及窗口变化场景。
实现原理
横向断点使用窗口宽度计算,纵向断点使用窗口的宽高比计算,详细断点分类可以参考 横纵向断点的设计原理 。根据不同断点对应的UX设计,使用横向及纵向断点判断并展示不同的页面布局。
使用 window (窗口) 的用 getWindowProperties() 接口获取窗口创建时的宽高计算横纵向断点。并使用on(‘windowSizeChange’) 接口监听窗口尺寸变化,窗口更新时获取变更后的窗口尺寸,更新横纵向断点。
开发步骤
- 在UIAbility的 onWindowStageCreate 生命周期回调中,获取窗口对象后使用 getWindowProperties() 接口获取窗口创建时的宽高计算断点,保存横纵向断点至全局状态存储中。并在on(‘windowSizeChange’)接口中监听窗口尺寸变化,窗口尺寸变化时计算横纵向断点,并更新全局状态存储。
- 页面使用全局状态存储获取当前窗口的横纵向断点,当横纵向断点更新时,按照实际页面逻辑调整窗口旋转策略、沉浸式样式等窗口属性或其他页面布局。
窗口横竖屏旋转适配
在开发一多应用时,需要兼容多设备上的旋转策略。当前应用开发中通常使用以下两种方案控制旋转策略。伴随着鸿蒙生态拓展,这两种方案也带来一些问题。
方案一:依靠设备类型或设备状态判断是否支持旋转
- 如果设备类型为平板,或折叠屏展开状态为展开态(仅考虑了折叠屏X系列的情况),则应用支持旋转。
- 其他情况下应用竖屏显示。
if (deviceInfo.deviceType === 'tablet' || display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED) {
windowObj.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
} else {
windowObj.setPreferredOrientation(window.Orientation.PORTRAIT);
}
采取当前方案会导致以下问题:
- 在小折叠屏手机(Pocket系列)的展开态内屏上不希望应用旋转,应用却能够旋转。
- 在小折叠屏手机(Pocket系列)的折叠态外屏(类方屏)上希望应用旋转,应用却不能够旋转。
- 在三折叠屏手机(XT系列)的三屏展开态内屏上希望应用旋转,应用却不能够旋转。
方案二:根据窗口宽高判断是否支持旋转
- 如果设备屏幕宽或高有一边不小于840vp,则应用支持旋转。
let displayInfo: display.Display = display.getDefaultDisplaySync();
let displayWidth: number = px2vp(displayInfo.width);
let displayHeight: number = px2vp(displayInfo.height);
if (displayWidth >= 840 || displayHeight >= 840) {
windowObj.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
} else {
windowObj.setPreferredOrientation(window.Orientation.PORTRAIT);
}
- 其他情况下应用竖屏显示。
采取当前方案会导致以下问题:
- 在部分直板机(pura70)或小折叠(Pocket2系列)的展开态上不希望应用旋转,因为设备的高度超过840vp,应用却能够旋转。
- 在折叠屏(X系列)展开态、小折叠(Pocket系列)折叠态、三折叠(XT系列)的双屏显示——类方屏上希望应用旋转,应用却不支持旋转。
实现原理
为解决以上方案的不足,根据当前鸿蒙生态设备的实际使用场景,建议旋转策略如下:
设备场景 | 是否需要支持横竖屏旋转 |
---|---|
手机(直板机) | 由应用决定是否需要支持,默认不支持 |
折叠屏-小折叠(Pocket系列) | 内屏(正面大屏):同直板机手机 外屏(背面小屏):类方屏需要支持 |
折叠屏(X系列) | 内屏(展开状态):类方屏需要支持 外屏(折叠状态):同直板机手机 |
三折叠(XT系列) | F态(单屏显示):同直板机手机 M态(双屏显示):类方屏需要支持 G态(三屏显示):需要支持 |
平板全场景 | 需要支持 |
PC/2in1 | 系统不响应应用方向事件,无法支持 |
根据以上常用设备类型上的应用窗口属性,将一多横竖屏旋转适配方案整理为:
- 应用窗口宽度和高度的最小值大于等于600vp(X系列折叠屏)时,推荐支持旋转。
- 应用窗口高宽比在[0.8, 1.2)区间内(类方屏)时,推荐支持旋转。
- 设备类型为平板时,推荐支持旋转。
- 其他情况推荐竖屏显示(直板机)。
示例代码
针对一多横竖屏推荐的旋转方案,通用的适配方案参考如下代码。
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BusinessError, deviceInfo } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
windowObj: window.Window | undefined = undefined;
private onWindowSizeChange: (data: window.Size) => void = (data: window.Size) => {
this.setDefaultOrientation();
}
// ...
setDefaultOrientation(): void {
const BREAKPOINT_MD = 600;
let windowRect: window.Rect = this.windowObj!.getWindowProperties().windowRect;
let windowWidthVp: number = px2vp(windowRect.width);
let windowHeightVp: number = px2vp(windowRect.height);
let aspectRatio: number = windowHeightVp / windowWidthVp;
if (Math.min(windowWidthVp, windowHeightVp) >= BREAKPOINT_MD || (aspectRatio < 1.2 && aspectRatio >= 0.8) || deviceInfo.deviceType === 'tablet') {
// Rotation supported.
this.windowObj?.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
} else {
// Portrait display.
this.windowObj?.setPreferredOrientation(window.Orientation.PORTRAIT);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.getMainWindow().then((windowObj) => {
this.windowObj = windowObj;
this.setDefaultOrientation();
this.windowObj.on('windowSizeChange', this.onWindowSizeChange);
}).catch((err: BusinessError) => {
hilog.error(0x0000, 'testTag', `Failed to obtain the main window. Cause code: ${err.code}, message: ${err.message}`);
});
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
}
// ...
}
窗口沉浸式页面适配
应用开发过程中,窗口默认为非沉浸式,即页面无法拓展至顶部状态栏及底部导航栏等避让区。此时存在避让区与页面颜色不一致以及底部导航栏空间无法利用的问题,因此需要手动设置沉浸式。
实现原理
窗口沉浸式页面通过应用页面延伸到状态栏和导航栏的方式实现。设置应用沉浸式布局后,为避免应用布局被顶部状态栏或底部导航栏遮挡,需要获取窗口规避区,在页面布局中进行避让。
使用 setWindowLayoutFullScreen(true) 方法设置窗口为全屏模式,页面布局会拓展到顶部状态栏及底部导航栏,此时部分布局会被遮挡。通过 getWindowAvoidArea() 接口以及 on(‘avoidAreaChange’) 接口获取到系统避让区高度,据此在页面顶部及底部使用padding属性避让状态栏和导航栏。
开发步骤
- 在UIAbility的 onWindowStageCreate 生命周期回调中,获取窗口对象后使用setWindowLayoutFullScreen(true)设置窗口为全屏模式。通过getWindowAvoidArea()接口获取当前系统避让区高度(包含顶部状态栏高度及底部导航栏高度),保存至全局状态存储中。并使用on(‘avoidAreaChange’)接口监听避让区变化获取避让区变化后的高度,同步至全局状态存储。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
// ...
private onAvoidAreaChange: (avoidArea: window.AvoidAreaOptions) => void = (avoidArea: window.AvoidAreaOptions) => {
if (avoidArea.type === window.AvoidAreaType.TYPE_SYSTEM) {
// Updates the height of the top status bar.
AppStorage.setOrCreate('topAvoidHeight', px2vp(avoidArea.area.topRect.height));
} else if (avoidArea.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
// Updated the bottom navigation bar.
AppStorage.setOrCreate('bottomAvoidHeight', px2vp(avoidArea.area.bottomRect.height));
}
};
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.getMainWindow((err, window: window.Window) => {
// ...
window.setWindowLayoutFullScreen(true);
let topAvoidHeight: window.AvoidArea = window.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topAvoidHeight', px2vp(topAvoidHeight.topRect.height));
let bottomAvoidHeight: window.AvoidArea =
window.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('bottomAvoidHeight', px2vp(bottomAvoidHeight.bottomRect.height));
});
window.on('avoidAreaChange', this.onAvoidAreaChange);
// ...
}
}
- 在应用页面中获取全局状态存储中的避让区高度,在页面顶部及底部使用padding属性避让状态栏和导航栏。
@Entry
@Component
struct Index {
@StorageLink('topAvoidHeight') topAvoidHeight: number = 0;
@StorageLink('bottomAvoidHeight') bottomAvoidHeight: number = 0;
build() {
Column() {
// ...
}
.height('100%')
.width('100%')
.padding({
top: this.topAvoidHeight,
bottom: this.bottomAvoidHeight
})
}
}
应用窗口化适配
在PC/2in1设备上,应用正常启动默认应该为自由窗口模式而非全屏。在应用适配PC/2in1设备过程中,实际存在应用启动默认全屏模式而非推荐的自由窗口,以及拖动自由窗口导致尺寸过小页面布局异常的问题。
实现原理
-
启动默认全屏问题
PC/2in1设备启动默认全屏是因为应用在其他设备上为实现沉浸式使用setWindowLayoutFullScreen(true)接口设置全屏导致的。需要判断PC/2in1设配类型,仅在非PC/2in1设备时设置全屏。 -
自由窗口过小导致页面布局异常问题
自由窗口支持自由拖动改变大小,可以通过开发实际规格配置窗口的最小尺寸,保证页面的正常显示。
示例代码
- 在UIAbility的onWindowStageCreate生命周期回调中获取窗口对象后,仅设备不为PC/2in1时窗口设置为全屏。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { deviceInfo } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
// ...
onWindowStageCreate(windowStage: window.WindowStage) {
// ...
let windowClass: window.Window | null = null;
windowStage.getMainWindow().then((data: window.Window) => {
// ...
windowClass = data;
if (deviceInfo.deviceType !== '2in1') {
windowClass.setWindowLayoutFullScreen(true);
}
});
// ...
}
}
- 在“src/main/ets/module.json5”文件的 abilities标签 中配置窗口的最小尺寸。
"abilities": [
{
// ...
"minWindowWidth": 331,
"minWindowHeight": 600,
// ...
],
应用窗口化标题栏沉浸式适配
在PC/2in1设备上,自由窗口默认包含支持移动、缩小、放大、关闭窗口的标题栏。开发者可通过设置应用窗口的标题栏不可见,将应用页面拓展至原标题栏区域,实现窗口沉浸式。效果图如下:
实现原理
PC/2in1设备窗口标题栏包含应用图标,应用名称,以及三键(全屏/还原、最小化、关闭)。通过设置标题栏不可见,隐藏应用图标及应用名称保留三键区的方式实现窗口沉浸式。
使用 setWindowDecorVisible(false) 设置窗口标题栏不可见,此时应用页面拓展至标题栏区域。使用 setWindowDecorHeight ()接口设置导航栏高度,控制右上角三键区显示以及显示高度。
示例代码
在UIAbility的 onWindowStageCreate 生命周期回调中,判断PC/2in1设备类型,调用setWindowDecorVisible(false)接口设置标题栏不可见,调用setWindowDecorHeight()接口设置右上角三键区的高度。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { deviceInfo } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
export default class EntryAbility extends UIAbility {
// ...
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
});
// ...
windowStage.getMainWindow().then((data: window.Window) => {
let deviceTypeInfo: string = deviceInfo.deviceType;
if (deviceTypeInfo === '2in1') {
// Set the title bar of the 2in1 device to be invisible after the loadContent() call takes effect.
data.setWindowDecorVisible(false);
// When the title bar is invisible, set the height of the title bar and control the height of the three buttons (full screen/restore, maximize, and close) in the upper right corner.
data.setWindowDecorHeight(64);
}
})
// ...
}
}
自由窗口的全屏沉浸式适配
在视频类应用适配PC/2in1设备时,需要实现视频播放页在自由窗口与全屏沉浸式切换功能。
实现原理
通过调用窗口状态操作的接口,实现应用全屏沉浸式与自由窗口的切换。以长视频应用为例,视频播放界面,在PC/2in1设备上需要支持沉浸式体验,适配指导如下:
状态 | 用户操作 | 使用接口 |
---|---|---|
应用窗口化播放视频 | 执行以下操作后应进入沉浸式视频全屏播放: * 鼠标双击视频区域 * 用户点击视频全屏按钮 | maximize() :设置窗口最大化时的布局效果,接口默认传入参数为ENTER_IMMERSIVE实现最大化时进入沉浸式布局效果。 |
沉浸式全屏播放视频 | 执行以下操作后应退出全屏沉浸式回到窗口状态: * 按下ESC键 * 点击退出全屏按钮 | recover():还原为浮动窗口,恢复为进入全屏前的大小和和位置。 |
开发步骤
- 双击视频区域以及点击视频全屏按钮分别采用TapGesture双击事件以及onClick点击事件实现。事件触发后获取当前窗口模式,若窗口模式为自由窗口,调用maximize()接口最大化窗口进入沉浸式全屏。
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Observed
export class WindowUtil {
private mainWindowClass?: window.Window;
// ...
maximize(): void {
if (this.mainWindowClass!.getWindowStatus() === window.WindowStatusType.FLOATING) {
this.mainWindowClass!.maximize()
.then(() => {
hilog.info(0x0000, 'testTag', '%{public}s', `Succeed in maximizing the window.`);
})
.catch((err: BusinessError) => {
hilog.error(0x0000, 'testTag', `Failed to maximize the window. Code: ${err.code}, message: ${err.message}`,
JSON.stringify(err) ?? '');
});
}
}
// ...
}
- 按下ESC按键以及点击退出全屏按钮分别采用onKeyEvent按键事件以及onClick点击事件实现。事件触发后获取当前窗口模式,若窗口模式为全屏模式,调用recover()恢复为进入全屏前的浮动窗口。
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Observed
export class WindowUtil {
private mainWindowClass?: window.Window;
// ...
recover(): void {
if (this.mainWindowClass!.getWindowStatus() === window.WindowStatusType.FULL_SCREEN) {
this.mainWindowClass!.recover()
.then(() => {
hilog.info(0x0000, 'testTag', '%{public}s', `Succeed in recovering the window.`);
})
.catch((err: BusinessError) => {
hilog.error(0x0000, 'testTag', `Failed to recover the window. Code: ${err.code}, message: ${err.message}`,
JSON.stringify(err) ?? '');
});
}
}
// ...
}
常见问题
不同设备默认的窗口模式是什么
手机、折叠屏、平板上应用的默认窗口模式为全屏模式,windowStatusType为1。PC/2in1上应用自由窗口的窗口模式为自由悬浮形式窗口模式,windowStatusType为4,全屏后为全屏模式,windowStatusType为1。窗口模式见下表:
名称 | 值 | 说明 |
---|---|---|
UNDEFINED | 0 | 表示APP未定义窗口模式。 |
FULL_SCREEN | 1 | 表示APP全屏模式。 |
MAXIMIZE | 2 | 表示APP窗口最大化模式。 |
MINIMIZE | 3 | 表示APP窗口最小化模式。 |
FLOATINGSPLIT_SCREEN | 4 5 | 表示APP自由悬浮形式窗口模式。 表示APP分屏模式。 |