【HarmonyOS】窗口管理实战指南
1.窗口管理
1.1窗口概述
1.1.1窗口模块的作用
- 展示应用和操作系统的UI界面
- 组织不同窗口之间的显示关系,即维护不同窗口之间的叠加层次和位置属性
- 提供窗口装饰,即窗口标题栏和窗口边框
- 提供窗口切换或者显隐的动效
- 协助系统进行事件分发(触摸事件需要根据窗口的位置)
1.1.2窗口类型
- 系统窗口:指的是完成系统特定功能的窗口。如音量条、壁纸、通知栏等
- 应用窗口
- 应用主窗口:显示应用界面,会在任务管理界面出现
- 应用子窗口:用于显示应用的弹窗、悬浮窗等辅助窗口,生命周期跟随应用主窗口
1.1.3主窗口的生命周期
1.1.3.1生命周期概述
在Stage模型中,一个UIAbility对应一个WindowStage、一个WindowStage对应一个应用主窗口;
由UIAbility通过WindowStage来管理主窗口并维护其生命周期,onWindowStageStage和onWindowStageDestroy即为主窗口的创建和销毁回调
每个UIAbility实例都会与一个WindowStage类实例相绑定,WindowStage类的作用主要是管理应用进程内的窗口,这个类包含一个主窗口。也就是说UIAbility实例通过WindowStage持有了一个主窗口,该主窗口为ArkUI提供了绘制区域
1.1.3.2主窗口的生命周期状态
窗口在进入前台、前后台切换以及退至后台时,会触发窗口相应的生命周期状态变化
- shown:应用从后台切换至前台时触发
- resumed:窗口进入可交互状态,窗口进入前台、分屏调整完毕都会触发
- paused:窗口进入不可交互状态。窗口在前台但是不可交互时触发,例如正在调整分屏的范围时
- hidden:窗口从前台切换至后台时触发
1.1.3.3监听生命周期变化
可以在onWindowStageCreate回调中使用on方法来监听生命周期变化
1.2管理应用窗口
1.2.1使用场景
- 设置应用主窗口属性以及目标页面
- 设置应用子窗口属性以及目标页面
- 设置窗口沉浸式
- 设置悬浮窗
- 监听窗口不可交互和可交互事件
1.2.2接口说明
- getMainWindow:获取WindowStage实例下的主窗口
- loadContent:为当前WindowStage的主窗口加载具体的页面
- createSubWindow:创建子窗口
- on:监听WindowStage生命周期的变化
- createWindow:创建子窗口或者系统窗口
- …
1.2.3配置应用主窗口
- 获取应用主窗口
windowStage.getMainWindow((err: BusinessError, data) => {})
- 设置主窗口属性
- 通过loadContent接口加载主窗口的目标页面
1.2.4配置应用子窗口
- 通过createSubWindow创建应用子窗口
windowStage_.createSubWindow('mySubWindow',(err: BusinessError, data)=>{if(err.code) {console.log('[test] 创建子窗口错误 ' + err.message+ err.code)return;}//返回的data即为Window对象}
- 设置子窗口属性
- 子窗口创建成功之后,可以改变其位置、大小,设置窗口背景色、亮度等
sub_windowClass = data;//移动窗口位置sub_windowClass.moveWindowTo(300,300);//调整窗口大小sub_windowClass.resize(500,500)
- 通过setUIContent和showWindow接口加载显示子窗口的具体内容
sub_windowClass.setUIContent('pages/subWindow',(err: BusinessError) =>{if(err.code){console.log('[test]子窗口加载目标页面错误 '+ err.code)return}if(!sub_windowClass){return;}//显示子窗口sub_windowClass.showWindow((err:BusinessError) =>{if(err.code){console.log('[test]子窗口显示错误')return;}})})
需要注意的是这里需要以err对象的错误码作为判断依据而不能使用err对象本身
- 不再需要子窗口时,通过destoryWindow接口销毁子窗口
- 运行结果如下
1.2.5设置窗口沉浸式
在看视频,玩游戏时,可以通过设置窗口的沉浸式能力,即隐藏状态栏、导航栏等不必要的系统窗口来提高用户体验
- 通过getMainWindow获取主窗口
- 实现沉浸式有以下两种方式
- 方式一:调用setWindowSystemBarEnable接口,设置导航栏、状态栏不显示来达到沉浸式效果
- 方式二:调用setWindowLayoutFullScreen接口,设置应用主窗口为全屏布局;随后调用setWindowSystemBarPropertiew接口来设置导航栏、状态栏的透明度、背景/文字颜色等属性,使其与主窗口显示一致来达到沉浸式效果
- 通过loadContent接口来加载窗口的内容
1.2.6设置悬浮窗(受限开放)
- 通过createWindow接口创建悬浮窗类型的窗口
- 设置悬浮窗的位置、大小等属性
- 通过setUIContent和showWindow接口显示悬浮窗的具体内容
- 不再需要悬浮窗时,使用destroyWindow接口销毁悬浮窗
1.2.7监听窗口不可交互与可交互事件
在创建WindowStage对象后可通过监听‘windowStageEvent’事件类型,来监听到窗口的生命周期变化
windowStage.on('windowStageEvent', (data) => {// 根据事件状态类型选择进行相应的处理if (data === window.WindowStageEventType.SHOWN) {console.info('current window stage event is SHOWN');// 应用进入前台,默认为可交互状态// ...} else if (data === window.WindowStageEventType.HIDDEN) {console.info('current window stage event is HIDDEN');// 应用进入后台,默认为不可交互状态// ...} else if (data === window.WindowStageEventType.PAUSED) {console.info('current window stage event is PAUSED');// 前台应用进入多任务,转为不可交互状态// ...} else if (data === window.WindowStageEventType.RESUMED) {console.info('current window stage event is RESUMED');// 进入多任务后又继续返回前台时,恢复可交互状态// ...}// ...});
1.3module.json5的metadata标签
1.3.1概述
metadata标签用于标识HAP的自定义信息,包含name、value、resoures三个子标签
- name:标识数据项的名称
- value:标识数据项的值
- resource:标识用户自定义数据的资源索引
2.3.2使用示例
- 使用metadata标签配置主窗口的默认大小和位置。其中name取值及其对应含义如下
- name为ohos.ability.window.height表示主窗口的默认高度
- name为ohos.ability.window.width表示主窗口的默认宽度
- …
- 使用metadata标签配置是否移除应用启动页
- name为ohos.remove.starting.window,value取值为true表示移除启动页,取值为false表示不移除,默认为false
- 配置主窗口启动时是否以最大化状态显示(仅在PC/2in1设备上生效)
- name为ohos.ability.window.isMaximize,value取值为true表示最大化启动、取值为false表示不以最大化状态启动
1.4在应用中使用画中画功能
1.4.1概述
1.4.1.1接口说明
-
isPiPEnable:判断当前系统是否支持画中画功能
-
create:创建画中画控制器(使用XComponent)
create(config: PiPConfiguration): Promise<PiPController>
- config:创建画中画控制器的参数
- context:一个BaseContext类型的对象
- componentController:表示原始的XComponent控制器
- navigationId:当前页面的导航id,当使用Navigation管理页面路由时,需要设置Navigation的id属性,并将该id设置到该字段,确保在还原场景时能够从画中画场景恢复到原窗口
- templateType:模版类型,用于区分视频播放、视频通话或视频会议,默认为视频播放
- controlGroups:画中画的可选控件列表
- config:创建画中画控制器的参数
-
create:创建画中画控制器(使用typeNode)
create(config: PiPConfiguration, contentNode: typeNode.XComponent): Promise<PiPController>
- config:画中画控制器参数
- contentNode:用于渲染画中画窗口中的内容,是一个XComponent类型的FrameNode节点
-
startPiP:启动画中画
-
stopPiP:停止画中画
-
setAutoStartEnable:设置是否在应用退至后台时自动启动画中画
-
…
1.4.1.2画中画的交互方式
- 单击画中画窗口:若控制层未显示则显示,三秒后隐藏;若已显示则隐藏
- 双击画中画窗口:放大或缩小画中画窗口
- 拖动画中画窗口
- 拖拽缩放画中画窗口大小
- 拖动删除画中画窗口
1.4.1.3配置画中画控制层可选控件
使用create创建画中画时,可通过在PiPConfiguration中新增PiPControlGroup类型的数组配置当前画中画窗口中的控件
- 视频播放场景通过配置VideoPlayControlGroup类
- 视频通话场景通过配置VideoPlayControlGroup类
- …
1.4.1.4在画中画上方展示自定义UI
在使用create创建画中画时,可通过在PiPConfiguration中传入customUIController来显示自定义UI
1.4.1.5实现方式
- 使用XComponent实现画中画功能:适用于应用通过Navigation管理页面或者Ability中只有一个页面的情况,这种实现方式无需应用管理页面
- 使用typeNode实现画中画:使用于所有场景,灵活度较高,但是需要应用自行管理页面
- 使用NDK实现画中画:适用依赖NDK接口开发的应用,需要应用自行管理页面
NDK也就是Native Development Kit 原生开发工具包,即使用C/C++编写的高性能工具库
在HarmonyOS开发中还有响应的SDK 即Software Development Kit软件开发工具包
1.4.2使用XComponent实现画中画
- 创建画中画控制器,并注册相应监听事件
startPip(){if(PiPWindow.isPiPEnabled() == false){return;}let config: PiPWindow.PiPConfiguration = {//表示上下文环境 BaseContext类型context: this.getUIContext().getHostContext() as Context,//原始XComponent控制器componentController: this.mXComponentController,//当前page导航id,单页面时无需设置navigationId:'',//模版类型,默认我视频播放templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY,//原始内容宽度px,用于确定画中画窗口比例,使用typeNode方式创建时默认为1920,否则默认为XComponent组件宽度contentWidth: 1920,//原始内容高度contentHeight:1080,//画中画控制面板的可选控件组列表controlGroups: [PiPWindow.VideoPlayControlGroup.VIDEO_PREVIOUS_NEXT],//画中画内容上方的自定义组件customUIController: undefined}PiPWindow.create(config).then((controller: PiPWindow.PiPController) => {//初始化画中画控制器this.pipController = controllerthis.initPipController()})}initPipController(){if(this.pipController == undefined){return}//设置是否需要在应用返回桌面时自动启动画中画,默认为falsethis.pipController.setAutoStartEnabled(true)//注册生命周期事件回调和控制事件回调,为便于演示此处不给出具体实现this.pipController.on('stateChange',(state: PiPWindow.PiPState, reason: string) => {})this.pipController.on('controlPanelActionEvent',(event: PiPWindow.PiPActionEventType,status?: number) => {})}
- 通过startPiP接口启动画中画
this.pipController.startPiP().then(()=>{console.log('[test]pip启动成功')}).catch(()=>{console.log('[test]pip启动失败')})
- 画中画媒体源更新后(切换视频),通过控制器的updateContentSize接口,根据新媒体源的尺寸信息来调整画中画的窗口比例
- 不需要时通过控制器的stopPiP接口关闭画中画
1.4.3使用typeNode实现画中画
1.4.3.1使用自由节点实现画中画
自由节点也就是不将节点添加到组件中
- 创建控制器,注册生命周期回调以及控制事件回调
- 通过主窗口UIContext类创建typeNode节点
// 创建typeNode节点makeTypeNode(ctx: UIContext) {if (this.xComponent === null || this.xComponent === undefined) {// 创建XComponent类型的typeNodethis.xComponent = typeNode.createNode(ctx, "XComponent", {type: XComponentType.SURFACE, // 类型设置为SURFACEcontroller: PipManager.getInstance().getXComponentController(), // 设置XComponentController});}}windowStage.getMainWindow().then((window) => {let ctx = window.getUIContext();AppStorage.setOrCreate('UIContext', ctx);// 通过主窗口UIContext,调用make方法,创建typeNode节点PipManager.getInstance().makeTypeNode(ctx);})
- 通过相对应的create接口来创建控制器实例
- …
- 通过startPiP接口启动画中画
- 根据媒体源变化调整窗口大小
- 不需要时关闭画中画
1.4.3.2使用Navigation导航时实现画中画
- 与使用自由节点不同的是需要创建一个自定义NodeController类
- 通过startPiP启动画中画,并且在画中画的aboutToStart生命周期中将typeNode节点从布局中移除
也可以在aboutToStart中调用路由栈的pop方法返回至上级页面,需要注意如果进行了pop操作,还需要在aboutToRestore生命周期中重新跳转至目标页面 - 根据媒体源变化调整窗口大小
- 不需要时关闭画中画,同时在aboutToStop生命周期中将typeNode节点重新添加至布局中