PC窗口适配开发实践
一、应用启动窗口布局适配
1. 场景介绍
应用启动时默认显示启动页,启动页是应用冷启动时显示的页面,作用是支持应用在冷启动时快速响应,优先执行启动动画(点击桌面图标,立即开始执行窗口动画,此时应用还未启动,没有任何内容可以显示,因此需要显示启动页)。当应用在onWindowStageCreate生命周期里调用moveWindowTo、resize等接口进行布局调整时,可能会出现窗口跳变现象,用户体验不好。
图10-1
2. 功能介绍
应用启动有两种方式:1、点击应用图标启动;2、通过startAbility 启动。
3. 规范标准
应用启动时设置主窗口的位置和大小有多种方式,按照生效优先级由高到低排序为:应用全屏启动 > startAbility中StartOptions指定大小位置 > 应用窗口使能记忆化 > 在 module.json5配置ability大小和位置 > 系统默认层叠规格。
3.1 应用全屏启动方式
- 该UIAbility对应的module.json5配置文件中abilities标签的supportWindowMode字段仅配置fullscreen选项或配置fullscreen和split但没有floating。
"abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "supportWindowMode": [ "fullscreen" ], } ]
"abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "supportWindowMode": [ "fullscreen", "split" ], } ]
- 在startAbility里通过StartOptions选项的windowMode参数设置为WINDOW_MODE_FULLSCREEN
this.context.startAbility(want, { windowMode: windowMode: AbilityConstant.WindowMode.WINDOW_MODE_FULLSCREEN });
- 在startAbility里通过StartOptions选项的supportWindowModes参数设置为FULL_SCREEN
this.context.startAbility(want, { supportWindowModes: [bundleManager.SupportWindowMode.FULL_SCREEN], });
- 在startAbility里通过StartOptions选项的supportWindowModes参数设置为FULL_SCREEN和SPLIT
this.context.startAbility(want, { supportWindowModes: [bundleManager.SupportWindowMode.FULL_SCREEN, bundleManager.SupportWindowMode.SPLIT], });
3.2 StartOptions指定大小位置
startAbility里通过StartOptions选项的windowLeft、windowTop、windowWidth、windowHeight设置窗口的位置和大小。
3.3 窗口记忆
应用可以通过setWindowRectAutoSave()接口开启窗口记忆,窗口记忆规则如下:
上一次窗口状态 |
记忆规则 |
---|---|
自由窗口 |
保留自由窗口的大小/位置,若超出工作区,则移入工作区完全显示 |
二分屏窗口 |
保留二分屏之前自由窗口的大小/位置,若超出工作区,则移入工作区完全显示 |
最大化窗口 |
保留最大化 |
沉浸式窗口 |
保留沉浸式之前自由窗口的大小/位置,若超出工作区,则移入工作区完全显示 |
最小化窗口 |
保留最小化之前自由窗口的大小/位置,,若超出工作区,则移入工作区完全显示 |
层叠规格 |
1. 当前实例窗口是自由窗口时,打开下一个实例窗口层叠时,大小要跟随。2. 当前实例窗口是最大化或全屏窗口时,打开下一个实例窗口层叠时,保持最大化。 |
// EntryAbility.ets import { UIAbility } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; export default class EntryAbility extends UIAbility { // ... onWindowStageCreate(windowStage: window.WindowStage): void { console.info('onWindowStageCreate'); try { let promise = windowStage.setWindowRectAutoSave(true); promise.then(() => { console.info('Succeeded in setting window rect auto-save'); }).catch((err: BusinessError) => { console.error(`Failed to set window rect auto-save. Cause code: ${err.code}, message: ${err.message}`); }); } catch (exception) { console.error(`Failed to set window rect auto-save. Cause code: ${exception.code}, message: ${exception.message}`); } } }
在同一个UIAbility下, 可记忆最后关闭的主窗口尺寸,也可针对每个主窗口尺寸单独进行记忆。只有在UIAbility启动模式为specified,且isSaveBySpecifiedFlag设置为true时,通过接口setWindowRectAutoSave(enabled: boolean, isSaveBySpecifiedFlag: boolean),才能针对每个主窗口尺寸进行单独记忆。
3.4 module.json5 配置ability大小和位置
- 配置应用启动时最大化,在module.json5的metadata属性字段添加配置ohos.ability.window.isMaximize,且满足supportWindowMode字段配置包含fullscreen选项。该方案可以避免在onWindowStageCreate里调用maximize出现闪烁现象。
"abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "supportWindowMode": [ "fullscreen" ], "metadata": [ { "name": "ohos.ability.window.isMaximize", "value": "true", }, ] }, ]
- ohos.ability.window.left : 指定窗口Left 坐标,格式为:(对齐方式)(+|-偏移量),当对齐方式为left 可以省略(+也要省略),偏移量为0时可以省略(单位为vp),对于 left-偏移量、right+偏移量、+|-偏移量 都属于非法情况,不生效
- ohos.ability.window.top: 指定窗口Top 坐标, 对齐方式包括: top|bottom|center, 当对齐方式为 top可以省略(+也要省略),偏移量为0时可以省略(单位为vp),对于top-偏移量、bottom+偏移量、+|-偏移量 都属于非法情况,不生效;
- ohos.ability.window.height: 指定窗口的高度,单位是vp;
- ohos.ability.window.width: 指定窗口的宽度,单位是vp限制条件;
- 如果窗口设置的大小和位置超出屏幕之外,会调整到当前屏幕内。窗口的宽高不能超过当前屏幕的宽高;
- 当left和top都不配置或配置不生效时按照系统层叠规则显示。
"abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "metadata": [ { "name": "ohos.ability.window.height", "value": "600", }, { "name": "ohos.ability.window.width", "value": "600", }, { "name": "ohos.ability.window.left", "value": "right-50", }, { "name": "ohos.ability.window.top", "value": "center+50", } ] }, ]
3.5 窗口层叠规则
3.5.1 默认窗口大小:
序号 |
规格名称 |
规格描述 |
---|---|---|
1 |
窗口默认大小 |
应用首次启动窗口默认大小,宽高各占屏幕尺寸的67%,在工作区(去除Dock和状态栏的屏幕区域)居中显示 |
2 |
系统尺寸限制 |
自由窗口的系统默认最小宽度为320VP,最小高度为72VP,最大宽度和高度都为1920VP; 应用未设置windowLimits时,通过window.getWindowLimits接口会默认返回前面系统的尺寸限制 |
3 |
默认窗口模式 |
支持floating模式的窗口,默认以自由窗口方式打开 |
3.5.2 层叠规则
- 找到除了置顶窗口外的最上层窗口(包括后台窗口),作为基准,进行层叠显示(分别向右和向下偏移37vp);
- 如果偏移后的窗口位置有部分超出了工作区,则将超出方向的坐标位置修改为工作区的起点位置;
- 支持多实例的应用,打开第二个窗口时,参考上一个多实例窗口的位置进行层叠。
二、应用窗口缩放适配
窗口大小改变有两种方式:1、通过窗口热区拖拽进行窗口缩放;2、通过resize接口对修改窗口大小。禁止窗口拖拽缩放有两种方式:1、setWindowLimits限制窗口大小;2、setResizeByDragEnabled禁止/启用主窗口和带装饰栏的子窗进行拖拽。主窗口和带标题栏的子窗口默认可以通过热区拖拽进行窗口大小缩放,不带标题栏的子窗口和悬浮窗不可以通过热区拖拽进行窗口大小缩放。
带装饰栏的子窗:
不带装饰栏子窗:
三、应用窗口拖拽移动适配
1、场景介绍
自绘制标题栏的应用窗口,实现窗口拖拽移动功能,很多应用根据拖拽移动时鼠标的位置,使用moveWindowTo来进行窗口移动,但会导致不跟手的问题,体验不好。并且该接口在扩展屏场景下不支持跨屏移动。API14以后建议通过startMoving实现跟随鼠标移动,实现和原生标题栏一样丝滑的拖拽移动体验,鼠标抬起时自动停止移动,在PC上推荐使用该接口进行拖拽移动,应用可以自定义拖拽区域,并且扩展屏场景下也有较好体验。支持使用stopMoving来停止窗口移动。
接口 |
功能区别 |
使用场景 |
---|---|---|
移动窗口位置 |
设置子窗的启动位置 |
|
跟手移动,抬手终止移动 |
无标题栏窗口移动,比如输入法 |
|
固定鼠标在窗口内的位置跟手移动 |
窗口分合场景 |
|
终止移动 |
绑定快捷键、拖拽删除 |
2、效果示例
startMoving和窗口标题栏移动效果对比:
图10-2
startMoving和startMoving(100,500)效果对比:
图10-3
3、适配指导
import { BusinessError } from '@kit.BasicServicesKit'; @Entry @Component struct Index { build() { Row() { Column() { Blank('160') .color(Color.Blue) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { try { windowClass.startMoving().then(() => { console.info('Succeeded in starting moving window.'); }).catch((err: BusinessError) => { console.error(`Failed to start moving. Cause code: ${err.code}, message: ${err.message}`); }); } catch (exception) { console.error(`Failed to start moving window. Cause code: ${exception.code}, message: ${exception.message}`); } } }) }.width('100%') }.height('100%').width('100%') } }
四、窗口布局适配
应用布局可以通过自适应布局和响应式布局来更新自身布局,避免出现截断、挤压、堆叠等现象。
五、窗口深浅色模式适配
当前系统存在深浅色两种显示模式,为了给用户更好的使用体验,应用需要适配深浅色模式。
参考链接:应用深浅色适配。
六、系统标题栏三键适配
1、场景介绍
1.1 应用存在自定义三键的诉求
- 使用自定义三键的应用,需要接口支持隐藏系统三键的能力;
1.2 使用系统三键的应用,存在控制系统三键样式的诉求
- 对于使用系统三键的应用,当窗口背景为深色时,为了清晰显示三键,应用需要接口支持控制三键深浅色模式的能力,使得三键的深浅色模式不跟随系统变化;
- 为了更好的适配应用的窗口布局,应用需要接口支持控制系统三键间距、关闭按钮右侧距窗口边距、三键交互底板大小的能力;
1.3 三键区域避让的诉求
- 在隐藏标题栏情况下,应用窗口自绘制标题栏区域自绘制控件,可能会与右侧系统三键的布局重叠,因此需要监听三键的区域变化
2、功能介绍
2.1 隐藏系统三键
- 通过接口setWindowTitleButtonVisible控制最大化、最小化、关闭按钮的显示与隐藏
setWindowTitleButtonVisible(isMaximizeButtonVisible: boolean, isMinimizeButtonVisible: boolean, isCloseButtonVisible?: boolean): void
- 参数
参数名 |
类型 |
必填 |
说明 |
---|---|---|---|
isMaximizeButtonVisible |
boolean |
是 |
设置最大化按钮是否可见,true为可见,false为隐藏。如果最大化按钮隐藏,那么在最大化场景下,也隐藏对应的还原按钮。 |
isMinimizeButtonVisible |
boolean |
是 |
设置最小化按钮是否可见,true为可见,false为隐藏。 |
isCloseButtonVisible |
boolean |
否 |
设置关闭按钮是否可见,true为可见,false为隐藏,默认值true。 |
2.2 控制系统三键样式
- 通过接口setDecorButtonStyle参数DecorButtonStyle的buttonBackgroundSize、spacingBetweenButtons、closeButtonRightMargin字段分别控制按钮高亮显示时的大小、按钮间距、关闭按钮右侧距窗口边距;
- 通过接口setDecorButtonStyle参数DecorButtonStyle的colorMode字段控制系统三键的深浅色模式;
setDecorButtonStyle(decorStyle: DecorButtonStyle): void
- 参数
参数名 |
类型 |
必填 |
说明 |
---|---|---|---|
decorStyle |
DecorButtonStyle |
是 |
要设置的装饰栏按钮样式。 |
- DecorButtonStyle
名称 |
类型 |
说明 |
---|---|---|
colorMode |
ConfigurationConstant.ColorMode |
颜色模式。深色模式下按钮颜色适配为浅色,浅色模式下按钮颜色适配为深色。未设置则默认跟随系统颜色模式。 |
buttonBackgroundSize |
number |
按钮高亮显示时的大小,取值范围20vp-40vp,默认值28vp。 |
spacingBetweenButtons |
number |
按钮间距,取值范围12vp-24vp,默认值12vp。 |
closeButtonRightMargin |
number |
关闭按钮右侧距窗口边距,取值范围8vp-22vp,默认值20vp。 |
2.3 三键区域避让
on(type: 'windowTitleButtonRectChange', callback: Callback<TitleButtonRect>): void
3、规范标准
3.1 自定义三键的应用需要满足下列标准
- 需要使用如下提供的三键资源开发窗口自定义三键
3.2 使用系统三键的应用,通过接口控制装饰栏三键样式时,需要满足如下标准
4、效果示例
4.1 隐藏系统三键效果图示
- 设置isMaximizeButtonVisible为false、isMinimizeButtonVisible为true、isCloseButtonVisible为true实现隐藏最大化按钮,显示最小化和关闭按钮
4.2 控制系统三键样式效果图示
- 左图buttonBackgroundSize为20vp、spacingBetweenButtons=24vp、 closeButtonRightMargin=8vp;
- 右图buttonBackgroundSize为40vp、spacingBetweenButtons=24vp、 closeButtonRightMargin=8vp;
5、适配文档
5.1 使用setWindowTitleButtonVisible接口(依赖API 14)设置主窗标题栏上的最大化、最小化、关闭按钮是否可见
// EntryAbility.ets import { UIAbility } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { // 加载主窗口对应的页面 windowStage.loadContent('pages/Index', (err) => { let mainWindow: window.Window | undefined = undefined; // 获取应用主窗口。 windowStage.getMainWindow().then( data => { mainWindow = data; console.info('Succeeded in obtaining the main window. Data: ' + JSON.stringify(data)); // 调用setWindowTitleButtonVisible接口,隐藏主窗标题栏最大化、最小化、关闭按钮。 mainWindow.setWindowTitleButtonVisible(false, false, false); } ).catch((err: BusinessError) => { if(err.code){ console.error(`Failed to obtain the main window. Cause code: ${err.code}, message: ${err.message}`); } }); }); } }
5.2 使用setDecorButtonStyle接口(依赖API 14)实现控制三键间距、右侧边距、交互底板大小和配置系统三键深浅色模式(不随系统变化)
import { UIAbility } from '@kit.AbilityKit'; import { ConfigurationConstant } from '@kit.AbilityKit'; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { try { windowStage.loadContent("pages/Index").then(() =>{ let windowClass = windowStage.getMainWindowSync(); let colorMode : ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT; let style: window.DecorButtonStyle = { colorMode: colorMode, buttonBackgroundSize: 24, spacingBetweenButtons: 12, closeButtonRightMargin: 20 }; windowClass.setDecorButtonStyle(style); console.info('Succeeded in setting the style of button. Data: ' + JSON.stringify(style)); }); } catch (exception) { console.error(`Failed to set the style of button. Cause code: ${exception.code}, message: ${exception.message}`); } } }
七、子窗口概念及适配指导
1、窗口类型及权限
类型:WINDOW_TYPE_APP_SUB_WINDOW
子窗分类:
窗口类型 |
使用场景、创建方式 |
约束 |
是否有设备差异 |
---|---|---|---|
普通应用子窗(一级) |
应用通过window.createWindow、windowStage.createSubWindow或 windowStage.createSubWindowWithOptions创建、 也支持arkui的弹窗组件通过showinsubwindow创建、 通过session.getUIExtensionWindowProxy()的createSubWindowWithOptions 方法创建、 应用通过window实例的windowClass.createSubWindowWithOptions 创建 |
1. 手机的系统窗和子窗不支持再创建子窗 2、手机UIExtension 子窗可以通过内部方法Create 创建 子窗(arkui的弹窗组件通过showinsubwindow) |
手机和PC有差异 |
多级子窗 |
(1)应用通过window实例的 windowClass.createSubWindowWithOptions (API12支持)创建 (2)通过挂载UEC组件的ExtensionWindow.createSubWindowWithOptions(API12支持)方法 (3)也支持arkui的弹窗组件通过showinsubwindow (API12支持) 创建 |
1.目前支持最大子窗为10级 |
仅支持PC、 PAD自由多窗、 PC应用上PAD普通模式 |
系统窗创建的子窗 |
(1) 悬浮窗通过window实例的windowClass.createSubWindowWithOptions(API15 支持)方法 (2)通过挂载UEC组件的ExtensionWindow.createSubWindowWithOptions(API12支持)方法 (3)arkui的弹窗组件通过showinsubwindow(API12支持)创建 |
仅PC支持 |
|
模态子窗 |
通过createSubWindowWithOptions 接口创建子窗时,设置子窗的模态属性 |
||
置顶模态子窗 |
通过windowStage.createSubWindowWithOptions、 windowClass.createSubWindowWithOptions、 ExtensionWindow.createSubWindowWithOptions 接口创建子窗时,设置子窗模态属性和置顶属性 |
无 |
|
置顶模应用子窗(API14) |
通过windowStage.createSubWindowWithOptions、 windowClass.createSubWindowWithOptions、 ExtensionWindow.createSubWindowWithOptions 接口创建子窗时,设置子窗的模应用属性和置顶属性 |
仅PC支持、PAD自由多窗 |
2、涉及API列表
编号 |
接口 |
描述 |
使用限制 |
---|---|---|---|
1 |
创建子窗口或者系统窗口。支持在FA模型下置顶类型为TYPE_APP, 创建应用子窗口 |
只支持FA模型 |
|
2 |
创建该WindowStage实例下的子窗口 |
||
3 |
创建该WindowStage实例下的子窗口 |
可以指定子窗口的模态属性、置顶模态属性 |
|
4 |
创建该window实例下的子窗口 |
可以指定子窗口的模态属性、置顶模态属性 |
|
4 |
子窗口创建参数。包括: title:子窗口标题 decorEnabled:子窗口是否显示装饰 isModal:子窗是否启用模态属性 isTopmost:子窗是否启用置顶属性(需要系统权限) modalityType:子窗口模态类型,仅当子窗口启用模态属性时生效。WINDOW_MODALITY表示子窗口模态类型为模窗口子窗,APPLICATION_MODALITY表示子窗口模态类型为模应用子窗。不设置,则默认为WINDOW_MODALITY。 |
||
5 |
获取当前应用内最上层的子窗口,若无应用子窗口,则返回应用主窗 |
||
6 |
设置子窗的模态属性是否启用 子窗口调用该接口时,设置子窗口模态属性是否启用。 启用子窗口模态属性后,其父级窗口不能响应用户操作,直到子窗口关闭或者子窗口的模态属性被禁用。 |
||
7 |
获取该WindowStage实例下的所有子窗口 |
||
8 |
显示当前窗口 |
||
9 |
销毁当前窗口 |
||
10 |
当调用对象为子窗口时,实现隐藏功能,不可在Dock栏中还原 |
3、功能规格:
3.1 模态窗口
模态窗口(Modal Window)是一种用户界面组件,通常以透明或半透明的浮动层覆盖在当前页面上,用于临时展示关键信息或引导用户完成特定操作(如保存信息)。它会暂时阻断用户与主窗口的交互,直到用户关闭该窗口,为强交互形式,会中断用户当前的操作流程,要求用户必须做出响应才能继续其他操作,通常用于需要向用户传达重要信息的场景。在鸿蒙系统中,模态窗口有如下几种类型:
3.1.1 模窗口子窗
窗口类型:WindowType.TYPE_APP。
适用场景与操作建议:
需要阻断某主窗及子窗的操作时:
- 使用条件开发者希望禁止用户对某个独立主窗口(及其子窗口)进行修改/交互。
- 实现方式
- 直接设置子窗为模态:将该主窗口中某个子窗口的ismodal属性设为true,modalityType属性设置为WINDOW_MODALITY或不设置。
- 新增模态子窗:对该主窗口或其子窗口动态创建一个新的模态子窗口。
- 示例场景
假设用户正在编辑器主窗口中修改文档,但存在未提交的更改时:
→ 弹出模态子窗提示“是否保存修改?”,此时主窗界面会被锁定,无法点击其他按钮,直至用户选择保存或取消。
注意事项
- 作用范围:确保模态子窗完全覆盖需阻断的主窗逻辑层级。
- 交互体验:避免滥用,仅在敏感操作(如数据丢失风险)时使用。仅在需模住单主窗时使用,多主窗场景下使用模应用子窗即可。
3.1.2 模态弹窗
窗口类型:WindowType.TYPE_DIALOG。
适用场景与操作建议同模窗口子窗,但模态弹窗只支持创建,不支持设置模态或取消模态。
3.1.3 模应用子窗
窗口类型:WindowType.TYPE_APP。
适用场景与操作建议:
需要阻断整个应用操作时:
- 使用条件
开发者希望禁止用户对所有主窗口及子窗口(整个应用)进行交互操作,直至关键流程完成(例如强制保存、权限验证等)。
- 实现方式
- 直接设置子窗模态为模应用:将该主窗口中某个子窗口的isModal属性设为true,modalityType属性设置为APPLICATION_MODALITY。
- 新增模应用子窗:对该主窗口或其子窗口动态创建一个新的模应用子窗口。
- 示例场景
用户打开多个主窗口修改了多个文档,未保存文档便点击“退出”按钮时:
→ 触发覆盖全应用的模态窗提示“是否保存未提交的修改?”,此时无法切换或操作本应用其他窗口(包括已打开的所有主窗及子窗)。
注意事项
- 作用范围:确保模应用子窗完全覆盖需阻断的主窗逻辑层级。
- 交互体验:避免滥用,仅在敏感操作(如数据丢失风险)时使用。仅在需模住多主窗时使用,单主窗场景下使用模窗口子窗即可。
3.2 层级
子窗层级始终保持在主窗口之上,无法调整到主窗下。
- 点击抬升
示例:主窗口下存在子窗口sub1,sub2。初始创建顺序为sub1、sub2。sub1设置层级抬升后,sub1高于sub2;点击sub2窗口,窗口响应点击事件,系统主动设置B层级抬升,层级变更为sub1>sub2;
- 子窗口、模态窗、模态子窗、置顶模态子窗、模应用子窗、置顶模应用子窗相对层级关系:
普通子窗口:类型为TYPE_APP,无特殊属性设置
模态子窗口:类型为TYPE_APP,设置isModal属性
模态窗:类型为TYPE_DIALOG
置顶模态子窗:类型为TYPE_APP,同时设置isModal和isTopmost属性
模应用子窗:类型为TYPE_APP,设置isModal和modalityType属性设置为APPLICATION_MODALITY。
置顶模应用子窗:类型为TYPE_APP,同时设置设置isModal和modalityType属性设置为APPLICATION_MODALITY和isTopmost属性
子窗口可在创建时设置模态属性和置顶属性,子窗口及模态窗的相对层级保持遵循以下原则:
1)置顶模应用子窗 > 模应用子窗 > 置顶模态子窗 > 模态窗(Dialog)、模态子窗口(后创建的层级更高) > 普通子窗口
2)不同类型间的窗口混排保持1)规则的前提下,相同类型的窗口层级顺序遵循层级的通用策略,即按照创建顺序,后创建的层级更高;
也可通过点击抬升、调用RaiseToAppTop等API抬升层级。
3.3 生命周期
- 子窗创建时需要绑定应用主窗口作为父窗口
- 子窗口的生命周期由应用发起控制,通过Create接口创建,Show/Hide接口管理显隐,Destroy接口销毁窗口实例。
- 生命周期跟随应用主窗口的生命周期。当父窗口显示/隐藏时,子窗口跟随显示/隐藏;应用主窗口销毁,子窗口一起销毁。
3.4 大小&位置
- 手机上应用子窗口的大小和显示受父窗口限制,无法超出父窗口
- pc上子窗同手机最大的不同,是默认可以超出父窗范围,按照屏幕坐标系显示。
- 调用MoveTo/Resize接口可以指定窗口的位置、宽高。
3.5 PC/2in1子窗口规格:
分类 |
规格 |
---|---|
子窗口坐标系 |
按照屏幕坐标系,可以超出父窗口 |
子窗口显示标题栏 |
默认不显示,可以通过SubWindowOptions中的decorEnabled字段显示标题栏 |
标题栏三键 |
默认只显示关闭按钮 |
多任务显示规则 |
进入多任务自动隐藏子窗口 |
分屏显示规则 |
主窗口进入/退出分屏,子窗口位置不变化;存在模态子窗的主窗口,不在待分屏列表显示 |
子窗口关闭回调 |
仅点击标题栏关闭按钮时触发 |
子窗口外观 |
可以设置子窗口的阴影和圆角 |
4、子窗口使用示例:
4.1 提示弹窗
用于提示用户操作,比如当文件修改未保存时,用户点击关闭键,提示用户是否关闭窗口
- 主窗口监听窗口关闭事件,弹窗提示用户是否关闭
closeListen() { try { let mainWindow = this.context.windowStage.getMainWindowSync(); mainWindow.on('windowWillClose', async () => { let subWindow = await mainWindow.createSubWindowWithOptions("windowStageClose", { title: '是否关闭窗口', decorEnabled: true, isModal: true }); await subWindow?.setUIContent("pages/PromptPage"); let width = 600; let height = 350; await subWindow.resize(width, height); let prop = mainWindow.getWindowProperties(); let displayInfo = display.getDisplayByIdSync(prop.displayId); let x = (displayInfo.availableWidth - width) / 2; let y = (displayInfo.availableHeight - height) / 2; await subWindow.moveWindowToGlobal(x, y, { displayId: prop.displayId }) await subWindow.showWindow(); return true; }); } catch (exception) { console.error(`Failed to enable the listener for window stage close event. Cause code: ${exception.code}, message: ${exception.message}`); } }
4.2 子窗同步主窗布局变化
4.2.1 场景介绍
在主窗上通过子窗创建一个蒙层,该蒙层需要和主窗口保持相同的位置和大小。
4.2.2 功能介绍
setFollowParentWindowLayoutEnabled 设置子窗或模态窗口(即WindowType为TYPE_DIALOG的窗口)的布局信息(position和size)是否跟随主窗,使用Promise异步回调。
- 只支持主窗的一级子窗或模态窗口使用该接口。
- 当子窗或模态窗口调用该接口后,立即使其布局信息与主窗完全一致并保持,除非传入false再次调用该接口,否则效果将持续。
- 当子窗或模态窗口调用该接口后,再调用moveTo、resize等修改布局信息的接口将不生效。
- 当子窗或模态窗口不再使用该功能后,不保证子窗或模态窗口的布局信息(position和size)为确定的值,需要应用重新进行设置。
// EntryAbility.ets import { window } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; import { UIAbility } from '@kit.AbilityKit'; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (loadError) => { if (loadError.code) { console.error(`Failed to load the content. Cause code: ${loadError.code}, message: ${loadError.message}`); return; } console.info("Succeeded in loading the content."); windowStage.createSubWindow("subWindow").then((subWindow: window.Window) => { if (subWindow == null) { console.error("Failed to create the subWindow. Cause: The data is empty"); return; } subWindow.setFollowParentWindowLayoutEnabled(true).then(() => { console.info("after set follow parent window layout"); }).catch((error: BusinessError) => { console.error(`setFollowParentWindowLayoutEnabled failed. ${error.code} ${error.message}`); }) }).catch((error: BusinessError) => { console.error(`createSubWindow failed. ${error.code} ${error.message}`); }) }); } }
4.3 主窗口移动时子窗跟随,且支持跨屏同显
4.3.1 场景介绍
1、部分应用会有主窗口和子窗口同时存在,当主窗移动时,子窗口需要跟随移动的场景;
2、新PC连接外接显示器场景下,子窗口在其直系各级父窗口处于拖拽移动或拖拽缩放过程时,该子窗口支持跨多个屏幕同时显示。
4.3.2 功能介绍
4.3.2.1 应用监听主窗的位置大小变化
- 使用on('windowRectChange')接口实现对主窗位置和大小变化的监听
on(type: 'windowRectChange', callback: Callback<RectChangeOptions>): void
4.3.2.2 根据主窗大小位置变化动态修改子窗口位置
- 使用moveWindowToGlobal等接口修改子窗位置,使之跟随主窗
moveWindowToGlobal(x: number, y: number, moveConfiguration?: MoveConfiguration): Promise<void>
4.3.2.3 取消监听主窗的位置大小变化
- 使用off('windowRectChange')接口实现对主窗位置和大小变化的监听
off(type: 'windowRectChange', callback?: Callback<RectChangeOptions>): void
4.3.2.4 设置子窗口在其父窗口处于拖拽移动或拖拽缩放过程时,该子窗口是否支持跨多个屏幕同时显示
- 通过setFollowParentMultiScreenPolicy()接口实现子窗口在其父窗口处于拖拽移动或拖拽缩放过程时支持跨多个屏幕同时显示。使用Promise异步回调。此接口仅可在2in1设备与自由多窗模式下使用。
setFollowParentMultiScreenPolicy(enabled: boolean): Promise<void>
4.3.3 效果示例
监听主窗的位置大小变化并使子窗跟随效果演示
图10-4
4.3.4 适配指导
import { window } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; try { let windowClass: window.Window = window.findWindow("subWindow"); let enabled: boolean = true; let promise = windowClass?.setFollowParentMultiScreenPolicy(enabled); promise.then(() => { console.info('Succeeded in setting the sub window supports multi-screen simultaneous display'); }).catch((err: BusinessError) => { console.error(`Failed to set the sub window supports multi-screen simultaneous display. Cause code: ${err.code}, message: ${err.message}`); }); // 需要跟随的父窗口 let parentWindow = this.context.windowStage.getMainWindowSync(); let rawRect = parentWindow?.getWindowProperties().windowRect; parentWindow.on("windowRectChange", (rect: window.RectChangeOptions)=>{ let offsetX = rect.rect.left - rawRect.left; let offsetY = rect.rect.top - rawRect.top; rawRect = rect.rect; let subWindowRect = windowClass?.getWindowProperties().windowRect; let parentWindowDisplayId = parentWindow?.getWindowProperties().displayId; let x = (subWindowRect?.left ?? 0) + offsetX; let y = (subWindowRect?.top ?? 0) + offsetY; windowClass?.moveWindowToGlobal(x, y, { displayId: parentWindowDisplayId }); }) } catch (exception) { console.error(`Failed to set the sub window supports multi-screen simultaneous display. Cause code: ${exception.code}, message: ${exception.message}`); }
八、悬浮窗适配指导
1.概念
悬浮窗口是一种特殊的应用窗口,窗口类型为WindowType.TYPE_FLOAT。其可以在已有的任务基础上,创建一个始终在前台显示的窗口。同时悬浮窗口具备在应用主窗口和对应Ability退至后台后,仍然可以在前台显示的能力。通常悬浮窗位于所有应用窗口之上,开发者可以创建悬浮窗,并对悬浮窗进行属性设置等操作。
悬浮窗口可以用于应用退至后台后使用小窗继续播放视频,或者为特定的应用创建悬浮球等快速入口。
2.功能介绍
2.1创建悬浮窗
创建悬浮窗类型的窗口,加载显示悬浮窗的具体内容。
- 创建悬浮窗需要应用具备ohos.permission.SYSTEM_FLOAT_WINDOW权限。
- 如果不进行大小设置,悬浮窗默认大小为所在屏幕大小,且会覆盖状态栏及顶栏。
window.createWindow创建子窗口或者系统窗口,使用Promise异步回调。loadContent根据当前工程中某个页面的路径为窗口加载具体页面内容,通过LocalStorage传递状态属性给加载的页面,使用Promise异步回调。多次调用该接口会先销毁旧的页面内容(即UIContent)再加载新的页面内容。showWindow显示当前窗口,使用Promise异步回调。
2.2 对悬浮窗进行属性设置
悬浮窗窗口创建成功后,可以改变其大小、位置等,还可以根据应用需要设置悬浮窗的圆角和阴影等属性。
moveWindowTo移动窗口位置。调用成功即返回,该接口返回后无法立即获取最终生效结果,如需立即获取,建议使用接口moveWindowToAsync()。
resize改变当前窗口大小,使用Promise异步回调。调用成功即返回,该接口返回后无法立即获取最终生效结果,如需立即获取,建议使用接口resizeAsync()。
setWindowCornerRadius设置子窗或悬浮窗的圆角半径值,使用Promise异步回调。在调用此接口之前调用getWindowCornerRadius()接口可以获得窗口默认圆角半径值。
setWindowShadowRadius设置窗口边缘阴影的模糊半径。
2.3 后台应用拉起悬浮窗
应用可以在后台创建并拉起悬浮窗。
2.4 悬浮窗启动主窗
已显示在前台的悬浮窗可以拉起被隐藏的主窗,或者拉起其他应用Ability。
- 对于2in1设备,如果应用已创建在前台显示的悬浮窗,当该应用退至后台时,无需校验ohos.permission.START_ABILITIES_FROM_BACKGROUND权限也可以拉起其他UIAbility。
- 如果应用配置了多实例,拉起应用时将会拉起新的UIAbility实例,启动Ability。
startAbility(want: Want, options?: StartOptions): Promise<void>
参考API:UIAbilityContext.startAbility。
2.5 销毁悬浮窗。
当不再需要悬浮窗时,可根据具体实现逻辑,使用destroyWindow接口销毁悬浮窗。销毁当前窗口,使用Promise异步回调。
3、规范标准
- 创建即悬浮窗类型的窗口,需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,该权限为受控开放权限,仅符合指定场景的在2in1设备上的应用可申请该权限。
- 常规情况如果需要拉起被隐藏的主窗,需要配置START_ABILITIES_FROM_BACKGROUND权限,但若应用存在处于前台的悬浮窗,可不配置以上权限。
- 在2in1设备上,悬浮窗创建数量不设上限。
4、效果示例
4.1 创建和销毁悬浮窗
图10-5
4.2 对悬浮窗进行属性设置
图10-6
4.3 后台应用拉起悬浮窗
图10-7
4.4 悬浮窗启动主窗
图10-8
5、适配文档
5.1 配置权限
- 在main/module.json5里配置的requestPermissions的权限列表中添加SYSTEM_FLOAT_WINDOW权限
// main/module.json5 "requestPermissions": [ { "name" : "ohos.permission.SYSTEM_FLOAT_WINDOW", "usedScene": { "abilities": [ "EntryAbility" ], "when":"always" } } ]
5.2 创建悬浮窗
// Index.ets import { common } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; async createFloatWindow(){ let subWindowName = "floatWindow"; let context = getContext(this) as common.UIAbilityContext; let config: window.Configuration = { name: subWindowName, windowType: window.WindowType.TYPE_FLOAT, ctx: context, }; try { let subWindow:window.Window = await window.createWindow(config); let storage: LocalStorage = new LocalStorage(); await subWindow.loadContent("pages/FloatWindow", storage); subWindow.showWindow(); storage.setOrCreate('name', subWindowName); } catch (exception) { console.error('Failed to create the window. Cause: ' + JSON.stringify(exception)); } }
5.3 对悬浮窗进行属性设置
//FloatWindow.ets import { window } from '@kit.ArkUI'; // 获得当前窗口对象 getWindow(windowName: string): window.Window | undefined { this.errorMsg = ''; let windowClass: window.Window | undefined = undefined; try { windowClass = window.findWindow(windowName); } catch (exception) { if (exception.code == 1300002) { this.errorMsg = `${windowName} 窗口名称不存在`; } else { this.errorMsg = `${windowName} 查找 ${JSON.stringify(exception)}` }2 console.error(`Failed to find the Window. Cause code: ${exception.code}, message: ${exception.message}`); return undefined; } return windowClass; } //设置属性 async resize() { let windowClass = this.getWindow("floatWindow"); if (!windowClass) { return } try { await windowClass.moveWindowTo(250, 200); await windowClass.resize(800, 600); await windowClass.setWindowCornerRadius(50); windowClass.setWindowShadowRadius(50); } catch (exception) { console.error('Failed to create the window. Cause: ' + JSON.stringify(exception)); } }
5.4 后台应用拉起悬浮窗
// Index.ets async createFloatWindowBackground(){ this.mainWin.minimize(); const createFloatWindow: string = await new Promise((resolve: Function) => { setTimeout(() => { this.createFloatWindow(); }, 5000); }); }
5.5 移动悬浮窗
通过 startMoving 接口让用户拖拽移动窗口
5.6 悬浮窗启动主窗
//FloatWindow.ets import { common, Want } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; async startMainAbility(){ let want: Want = { deviceId: '', bundleName: 'com.example.MyBundleName', abilityName: 'MyAbility', }; try{ let context = getContext(this) as common.UIAbilityContext; context.startAbility(want, (err: BusinessError) => { if (err.code) { // 处理业务逻辑错误 console.error(`hide startAbility failed, code is ${err.code}, message is ${err.message}`); return; } // 执行正常业务 console.info('show startAbility succeed'); }); } catch (err) { // 处理入参错误异常 console.error(`show startAbility failed, code is ${err.code}, message is ${err.message}`); } }
5.7 销毁悬浮窗
// FloatWinodw.ets closeSubWindow() { let windowClass = this.getWindow(this.inputWinName); if (!windowClass) { return } try { let stage = AppStorage.get<window.WindowStage>(this.inputWinName); if (stage) { this.errorMsg = `${this.inputWinName} 主窗不能调用destroyWindow`; return; } windowClass.destroyWindow().then(()=>{ this.errorMsg = `${this.inputWinName} 子窗关闭成功`; }); } catch (exception) { this.errorMsg = `${this.inputWinName} 窗口关闭失败`; console.error(`Failed to destroy the window. Cause code: ${exception.code}, message: ${exception.message}`); } }
九、Hopper场景适配指导-监听设备状态变化与屏幕可用区域变化
1.概念
Hopper机器比较特殊,区别于一般的折叠机,Hopper机器有悬停态、展开横屏、展开竖屏三种形态。应用可以监听设备状态变化与屏幕可用区域变化,动态调整窗口的位置大小等属性,并做一些特殊设计,避免一成不变,导致出现问题。
2.功能介绍
2.1获取屏幕对象
display对象中包含屏幕宽高,屏幕可用区域宽高等重要信息。
display.getDefaultDisplaySync获取当前默认的display对象。
display.getPrimaryDisplaySync获取主屏信息。除2in1之外的设备获取的是设备自带屏幕的Display对象;2in1设备外接屏幕时获取的是当前主屏幕的Display对象;2in1设备没有外接屏幕时获取的是自带屏幕的Display对象。
display.getAllDisplays获取当前所有的display对象。
display.getDisplayByIdSync根据displayId获取对应的display对象。
参考链接:Display。
2.2 设备变化监听
显示设备变化的监听。
display.on('add'|'remove'|'change')开启显示设备变化的监听。
display.off('add'|'remove'|'change')关闭显示设备变化的监听。
2.3 获取设备折叠状态
display.getFoldStatus获取可折叠设备的当前折叠状态。
2.4 获取可用区域或折痕区
获取设备屏幕可用区域。
getAvailableArea获取当前设备屏幕的可用区域,使用Promise异步回调。仅支持2in1设备。
on('availableAreaChange')开启当前设备屏幕的可用区域监听。当前设备屏幕有可用区域变化时,触发回调函数,返回可用区域。仅支持2in1设备。
off('availableAreaChange')关闭当前设备屏幕可用区域变化的监听。仅支持2in1设备。
display.getCurrentFoldCreaseRegion在当前显示模式下获取折叠折痕区域。
3、规范标准
- 及时感知屏幕可用区域变化,调整窗口位置或宽高,避免超出可用区域。
4、效果示例
4.1 在悬停态与展开态互相切换、展开态竖屏与展开横屏互相切换时感知可用区域与设备状态变化,并打印信息
图10-9
5、适配代码示例
5.1 感知设备状态变化
@Component struct DeviceStatePage { @State orientation: number = display.getDefaultDisplaySync().orientation; // 屏幕当前显示的方向,0为竖屏,1为横屏,2为反向竖屏,3为反向横屏 @State foldStatus: number = display.getFoldStatus(); // 折叠状态 1为全屏 3为半折叠 @State deviceStateMessage: string = '应用启动'; aboutToAppear() { // 磁吸态 监听 display.on('remove', (data) => { console.log(`remove`); const defaultDisplay = display.getDefaultDisplaySync(); // 获取默认屏幕可用区域,Hopper上为上半屏 const id = defaultDisplay.id; const width: number = defaultDisplay.width; const height: number = defaultDisplay.height; // width < height时,为悬停态转竖屏 if (width > height) { console.log(`display-- 磁吸态 remove${data},id:${id},width:${width},height:${height}`); this.deviceStateMessage = `设备状态:磁吸态 remove${data},id:${id},width:${width},height:${height}`; } }) //监听屏幕状态改变 display.on("change", async () => { console.log(`change`); const newDisplay = display.getDefaultDisplaySync(); const newOrientation = newDisplay.orientation; const newFoldStatus = display.getFoldStatus(); // if(window.Orientation.AUTO_ROTATION) //跟随传感器自动旋转,可以旋转到竖屏、横屏、反向竖屏、反向横屏四个方向。 // display.on(‘change’)其他的一些场景也会触发该回调,比如dpi,屏幕基础属性变化也会有通知。 // 先获取到原来的屏幕横竖屏状态,在回调函数中获取当前的横竖屏状态,判断两者是否一致,如果不一致再去执行响应的回调函数。 if (newOrientation !== this.orientation) { this.orientation = newOrientation switch (this.orientation) { case display.Orientation.PORTRAIT: console.log(`display--竖屏状态:orientation = ${this.orientation}`); this.deviceStateMessage = `设备状态:竖屏状态:orientation = ${this.orientation}`; break; case display.Orientation.LANDSCAPE: console.log(`display--横屏状态:orientation = ${this.orientation}`); this.deviceStateMessage = `设备状态:横屏状态:orientation = ${this.orientation}`; break; case display.Orientation.PORTRAIT: console.log(`display--反向竖屏状态:orientation = ${this.orientation}`); this.deviceStateMessage = `设备状态:反向竖屏状态:orientation = ${this.orientation}`; break; default: console.log(`display--反向横屏状态:orientation = ${this.orientation}`); this.deviceStateMessage = `设备状态:反向横屏状态:orientation = ${this.orientation}`; } } if (this.orientation === 0 && this.foldStatus !== newFoldStatus) { console.log(`display--竖屏折叠态转换:orientation : ${this.orientation},foldStatus : ${this.foldStatus}`); if (newFoldStatus === 3) { console.log(`display--竖屏折叠态转换:竖转悬停`); this.deviceStateMessage = `设备状态:竖屏折叠态转换:竖转悬停`; } if (newFoldStatus === 1) { console.log(`display--竖屏折叠态转换:悬停转竖`); this.deviceStateMessage = `设备状态:竖屏折叠态转换:悬停转竖`; } this.foldStatus = newFoldStatus } }); } build() { Column() { Text(this.deviceStateMessage) .fontSize(24) .fontWeight(700) .width('100%') .textAlign(TextAlign.Center) .padding({ left: 16 }) .fontFamily('HarmonyHeiTi-Bold') .lineHeight(33) } .height('100%') .width('100%') .backgroundColor('#F1F3F5') } }
5.2 感知可用区域变化
@Component struct AvailableAreaPage { private context = (this.getUIContext()?.getHostContext() as common.UIAbilityContext); private windowStage = this.context.windowStage; @State windowClass: window.Window = this.windowStage.getMainWindowSync(); @State displayId: number | undefined = this.windowClass.getWindowProperties().displayId; @State displayInfo: display.Display = display.getDisplayByIdSync(this.displayId); @State defaultDisplay: display.Display = display.getDefaultDisplaySync(); @State availableAreaMessage: string = `当前窗口所在屏幕:displayId: ${this.displayId}, availableWidth: ${this.displayInfo.availableWidth}, availableHeight: ${this.displayInfo.availableHeight}`; private callback: Callback<display.Rect> = (data: display.Rect) => { console.info('Listening enabled. defaultDisplay availableArea Data: ' + JSON.stringify(data)); this.displayId = this.windowClass.getWindowProperties().displayId; this.displayInfo = display.getDisplayByIdSync(this.displayId); this.availableAreaMessage = `当前窗口所在屏幕:displayId: ${this.displayId}, availableWidth: ${this.displayInfo.availableWidth}, availableHeight: ${this.displayInfo.availableHeight}`; }; aboutToAppear() { //监听窗口大小位置改变与窗口所在屏幕的可用区域改变 try { this.windowClass.on('windowRectChange', (data: window.RectChangeOptions) => { console.info(`Succeeded window rect changes. Data: ${JSON.stringify(data)}`); this.displayId = this.windowClass.getWindowProperties().displayId; this.displayInfo = display.getDisplayByIdSync(this.displayId); this.availableAreaMessage = `当前窗口所在屏幕:displayId: ${this.displayId}, availableWidth: ${this.displayInfo.availableWidth}, availableHeight: ${this.displayInfo.availableHeight}`; }); } catch (exception) { console.error(`Failed to disable the listener for window rect changes. Cause code: ${exception.code}, message: ${exception.message}`); } //监听默认屏幕的可用区域变化与窗口所在屏幕的可用区域变化 try { this.defaultDisplay.on("availableAreaChange", this.callback); } catch (exception) { console.error(`Failed to register callback. Code: ${exception.code}, message: ${exception.message}`); } } build() { Column() { Text(this.availableAreaMessage) .fontSize(24) .fontWeight(700) .width('100%') .textAlign(TextAlign.Center) .padding({ left: 16 }) .fontFamily('HarmonyHeiTi-Bold') .lineHeight(33) } .height('100%') .width('100%') .backgroundColor('#F1F3F5') } }
更多推荐
所有评论(0)