【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接口说明

  1. getMainWindow:获取WindowStage实例下的主窗口
  2. loadContent:为当前WindowStage的主窗口加载具体的页面
  3. createSubWindow:创建子窗口
  4. on:监听WindowStage生命周期的变化
  5. createWindow:创建子窗口或者系统窗口
  6. ...

1.2.3配置应用主窗口

  1. 获取应用主窗口
typescript 复制代码
windowStage.getMainWindow((err: BusinessError, data) => {})
  1. 设置主窗口属性
  2. 通过loadContent接口加载主窗口的目标页面

1.2.4配置应用子窗口

  1. 通过createSubWindow创建应用子窗口
typescript 复制代码
windowStage_.createSubWindow('mySubWindow',(err: BusinessError, data)=>{
      if(err.code) {
        console.log('[test] 创建子窗口错误   ' + err.message+ err.code)
        return;
      }
      //返回的data即为Window对象
 }
  1. 设置子窗口属性
  • 子窗口创建成功之后,可以改变其位置、大小,设置窗口背景色、亮度等
typescript 复制代码
	 sub_windowClass = data;
      //移动窗口位置
      sub_windowClass.moveWindowTo(300,300);
      //调整窗口大小
      sub_windowClass.resize(500,500)
  1. 通过setUIContent和showWindow接口加载显示子窗口的具体内容
typescript 复制代码
      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对象本身

  1. 不再需要子窗口时,通过destoryWindow接口销毁子窗口
  2. 运行结果如下

1.2.5设置窗口沉浸式

在看视频,玩游戏时,可以通过设置窗口的沉浸式能力,即隐藏状态栏、导航栏等不必要的系统窗口来提高用户体验

  1. 通过getMainWindow获取主窗口
  2. 实现沉浸式有以下两种方式
    • 方式一:调用setWindowSystemBarEnable接口,设置导航栏、状态栏不显示来达到沉浸式效果
    • 方式二:调用setWindowLayoutFullScreen接口,设置应用主窗口为全屏布局;随后调用setWindowSystemBarPropertiew接口来设置导航栏、状态栏的透明度、背景/文字颜色等属性,使其与主窗口显示一致来达到沉浸式效果
  3. 通过loadContent接口来加载窗口的内容

1.2.6设置悬浮窗(受限开放)

  1. 通过createWindow接口创建悬浮窗类型的窗口
  2. 设置悬浮窗的位置、大小等属性
  3. 通过setUIContent和showWindow接口显示悬浮窗的具体内容
  4. 不再需要悬浮窗时,使用destroyWindow接口销毁悬浮窗

1.2.7监听窗口不可交互与可交互事件

在创建WindowStage对象后可通过监听'windowStageEvent'事件类型,来监听到窗口的生命周期变化

typescript 复制代码
 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使用示例

  1. 使用metadata标签配置主窗口的默认大小和位置。其中name取值及其对应含义如下
  • name为ohos.ability.window.height表示主窗口的默认高度
  • name为ohos.ability.window.width表示主窗口的默认宽度
  • ...
  1. 使用metadata标签配置是否移除应用启动页
  • name为ohos.remove.starting.window,value取值为true表示移除启动页,取值为false表示不移除,默认为false
  1. 配置主窗口启动时是否以最大化状态显示(仅在PC/2in1设备上生效)
  • name为ohos.ability.window.isMaximize,value取值为true表示最大化启动、取值为false表示不以最大化状态启动

1.4在应用中使用画中画功能

1.4.1概述

1.4.1.1接口说明
  1. isPiPEnable:判断当前系统是否支持画中画功能

  2. create:创建画中画控制器(使用XComponent)

    typescript 复制代码
    create(config: PiPConfiguration): Promise<PiPController>
    • config:创建画中画控制器的参数
      • context:一个BaseContext类型的对象
      • componentController:表示原始的XComponent控制器
      • navigationId:当前页面的导航id,当使用Navigation管理页面路由时,需要设置Navigation的id属性,并将该id设置到该字段,确保在还原场景时能够从画中画场景恢复到原窗口
      • templateType:模版类型,用于区分视频播放、视频通话或视频会议,默认为视频播放
      • controlGroups:画中画的可选控件列表
  3. create:创建画中画控制器(使用typeNode)

    typescript 复制代码
    create(config: PiPConfiguration, contentNode: typeNode.XComponent): Promise<PiPController>
    • config:画中画控制器参数
    • contentNode:用于渲染画中画窗口中的内容,是一个XComponent类型的FrameNode节点
  4. startPiP:启动画中画

  5. stopPiP:停止画中画

  6. setAutoStartEnable:设置是否在应用退至后台时自动启动画中画

  7. ...

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实现画中画

  1. 创建画中画控制器,并注册相应监听事件
typescript 复制代码
  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 = controller
        this.initPipController()
      })
  }
  initPipController(){
    if(this.pipController == undefined){
      return
    }
    //设置是否需要在应用返回桌面时自动启动画中画,默认为false
    this.pipController.setAutoStartEnabled(true)
    //注册生命周期事件回调和控制事件回调,为便于演示此处不给出具体实现
    this.pipController.on('stateChange',(state: PiPWindow.PiPState, reason: string) => {
      
    })
    this.pipController.on('controlPanelActionEvent',(event: PiPWindow.PiPActionEventType,status?: number) => {
      
    })
  }
  1. 通过startPiP接口启动画中画
typescript 复制代码
this.pipController.startPiP()
          .then(()=>{
            console.log('[test]pip启动成功')
          })
          .catch(()=>{
            console.log('[test]pip启动失败')
          })
  1. 画中画媒体源更新后(切换视频),通过控制器的updateContentSize接口,根据新媒体源的尺寸信息来调整画中画的窗口比例
  2. 不需要时通过控制器的stopPiP接口关闭画中画

1.4.3使用typeNode实现画中画

1.4.3.1使用自由节点实现画中画

自由节点也就是不将节点添加到组件中

  1. 创建控制器,注册生命周期回调以及控制事件回调
  • 通过主窗口UIContext类创建typeNode节点
typescript 复制代码
// 创建typeNode节点
  makeTypeNode(ctx: UIContext) {
    if (this.xComponent === null || this.xComponent === undefined) {
      // 创建XComponent类型的typeNode
      this.xComponent = typeNode.createNode(ctx, "XComponent", {
        type: XComponentType.SURFACE, // 类型设置为SURFACE
        controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      });
    }
  }
  
  windowStage.getMainWindow().then((window) => {
      let ctx = window.getUIContext();
      AppStorage.setOrCreate('UIContext', ctx);
      // 通过主窗口UIContext,调用make方法,创建typeNode节点
      PipManager.getInstance().makeTypeNode(ctx);
    })
  • 通过相对应的create接口来创建控制器实例
  • ...
  1. 通过startPiP接口启动画中画
  2. 根据媒体源变化调整窗口大小
  3. 不需要时关闭画中画
1.4.3.2使用Navigation导航时实现画中画
  1. 与使用自由节点不同的是需要创建一个自定义NodeController类
  2. 通过startPiP启动画中画,并且在画中画的aboutToStart生命周期中将typeNode节点从布局中移除
    也可以在aboutToStart中调用路由栈的pop方法返回至上级页面,需要注意如果进行了pop操作,还需要在aboutToRestore生命周期中重新跳转至目标页面
  3. 根据媒体源变化调整窗口大小
  4. 不需要时关闭画中画,同时在aboutToStop生命周期中将typeNode节点重新添加至布局中
相关推荐
折翼的恶魔4 小时前
前端学习之布局
前端·学习
颜酱4 小时前
理解 Webpack 的构建过程(实现原理),并实现一个 mini 版
前端·javascript·webpack
haidragon4 小时前
第 1 周 —— **OSI 之旅开始了**
前端
Python私教4 小时前
React 19 如何优雅整合 Ant Design v5 与 Tailwind CSS v4
前端·css·react.js
拳打南山敬老院4 小时前
🚀 为什么 LangChain 不做可视化工作流?从“工作流”到“智能体”的边界与融合
前端·人工智能·后端
前端老鹰4 小时前
解锁 JavaScript 字符串补全魔法:padStart()与 padEnd()
前端·javascript
刺客_Andy4 小时前
React 第四十一节Router 中 useActionData 使用方法案例以及注意事项
前端·react.js
一心只读圣贤书4 小时前
解决.spec-workflow-mcp配置报错
前端
日月之行_5 小时前
还在用ref操作DOM?Vue 3.5 useTemplateRef如何彻底改变DOM引用方式
前端