目录

应用适配自由窗口

场景介绍

自由窗口状态下默认支持窗口大小缩放,且主窗口默认存在标题栏,与非自由窗口存在差异,为避免应用在自由窗口状态下出现界面截断、遮挡或控件叠加等问题,需要应用进行适配。

本章将列举应用可能出现的布局问题,并提供相应的解决方案。

查询当前窗口是否处于自由窗口状态

应用在自由窗口状态和非自由窗口状态下,如果需要做差异化布局,可通过接口查询当前窗口是否处于自由窗口状态。

接口 功能 使用场景
isInFreeWindowMode() 查询当前窗口是否处于自由窗口状态 检测窗口是否处于自由窗口状态,以便根据实际情况调整布局或功能。
on('freeWindowModeChange') 开启自由窗口状态变化事件的监听 自由窗口状态发生变化时,通知应用,以便根据实际情况调整布局或功能。
off('freeWindowModeChange') 关闭自由窗口状态变化事件的监听 应用退出前,关闭自由窗口状态变化事件的监听。

限制自由窗口尺寸

自适应布局可以保证窗口尺寸在一定范围内变化时,页面的显示是正常的。当窗口尺寸变化较大时,就需要额外借助响应式布局能力(如断点等)调整页面结构以保证显示正常。通常每个断点都需要开发者精心适配,以获得最佳的显示效果,考虑到设计及开发成本等实际因素的限制,应用不可能适配从零到正无穷的所有窗口宽度。

不同设备或不同设备状态下,系统默认的自由窗口尺寸的调节范围可能不同。开发者可通过以下三种方式限制自由窗口的最大和最小尺寸。

  • 如果开发者希望针对不同设备类型配置不同的最小值,可通过多HAP工程实现。

  • 窗口尺寸限制的最终生效结果由默认系统限制、应用配置和运行时设置的数据取交集得到,优先级从高到低依次为:

    1. 应用通过setWindowLimits()设置自由窗口尺寸限制。

    2. 应用使用startAbility()接口拉起主窗口时通过StartOptions指定自由窗口尺寸限制。

    3. 使用module.json5配置文件中的abilities标签限制自由窗口的最大和最小尺寸。

    4. 默认系统限制(对于不同产品和窗口类型,windowLimits系统默认限制存在差异)。

  • 自由窗口状态下,主窗口可通过setAspectRatio(ratio: number)设置窗口内容布局的比例,如果先设置了WindowLimits,后设置的ratio与其冲突,会返回错误码401;如果先设置了ratio,后设置的WindowLimits与其冲突,窗口的宽高比可能会不跟随设置的宽高比(ratio)。

调整布局避让标题栏和窗口三键

自由窗口状态下,应用根据窗口尺寸动态布局时,如果适配得当,可让页面元素合理显示,还能解决不同设备上的兼容性问题。

但需注意,自由窗口状态下窗口尺寸包含了标题栏区域和窗口内的可绘制区域,当需要计算页面元素相对于窗口的位置时,需要考虑避让标题栏。

标题栏是指窗口左上角的应用图标、标题以及与窗口等宽的顶部区域。

示意图中,windowRect为窗口尺寸,类型为Rect;drawableRect为可绘制区域尺寸,类型为Rect;decorHeight为标题栏高度,类型为number,单位为vp;density为本窗口所处屏幕的系统显示大小缩放系数,类型为number;w为窗口宽度,类型为number,单位为px;h为窗口高度,类型为number,单位为px。

典型场景及对应方案如下:

如果应用页面底部是固定高度的功能按钮,在计算底部功能按钮位置时,若只用窗口高度减去底部功能按钮高度,则会导致底部功能按钮向下偏移一个标题栏高度的位置,从而出现显示不全的情况。

有以下三种方式解决以上问题:

窗口三键是指窗口右上角的窗口最大化/还原、窗口最小化和关闭窗口三个按钮。

如果应用希望在窗口内有更大的可绘制区域,可以通过隐藏标题栏,并适配窗口三键的位置和尺寸。

典型场景及对应方案如下:

应用在页面顶部绘制与窗口三键并排的自定义工具栏或者是页签栏,可以按以下步骤进行适配。

  1. 通过setWindowDecorVisible()接口隐藏标题栏。

  2. 通过getTitleButtonRect()接口或on('windowTitleButtonRectChange')接口获取到窗口三键的位置和尺寸,在页面顶部元素布局时,去除窗口三键的区域。

     // EntryAbility.ets
     // 示例应用的入口
     import { UIAbility } from '@kit.AbilityKit';
     import { window } from '@kit.ArkUI';
     export default class EntryAbility extends UIAbility {
       onWindowStageCreate(windowStage: window.WindowStage): void {
         windowStage.loadContent('pages/Index', (err) => {
           if (err.code) {
             console.info('Failed to load the content. Cause: %{public}s', JSON.stringify(err));
             return;
           }
           AppStorage.setOrCreate<window.WindowStage>('windowStage', windowStage);
         });
       }
     }
     // Index.ets
     // 示例应用的主页面
     import { window } from '@kit.ArkUI';
    
     @Entry
     @Component
     struct Index {
       @State message: string = 'Top layout area, do not hide the title bar.';
       private mainWindow: window.Window | null = null;
       @State private topAreaHeight: number = 50;
       @State private topAreaWidth: number | string = '100%';
       @State private titleButtonRect: window.TitleButtonRect = {
         right: 0,
         top: 0,
         width: 0,
         height: 0
       };
       @State private windowSize: window.Size = { width: 0, height: 0 }
    
       aboutToAppear(): void {
         let windowStage = AppStorage.get<window.WindowStage>('windowStage');
         if (!windowStage) {
           return;
         }
         this.mainWindow = windowStage.getMainWindowSync();
         this.topAreaHeight = this.mainWindow.getWindowDecorHeight();
         this.mainWindow.on("windowSizeChange", (data) => {
           this.windowSize = data;
           this.topAreaWidth = this.getUIContext().px2vp(this.windowSize.width) - this.titleButtonRect.width;
         })
       }
    
       private updateTitleButtonRect(): void {
         if (!this.mainWindow) {
           return;
         }
         try {
           let WindowProperties = this.mainWindow.getWindowProperties();
           this.windowSize.width = WindowProperties.drawableRect.width;
           this.titleButtonRect = this.mainWindow.getTitleButtonRect();
           this.topAreaHeight = this.titleButtonRect.height;
           this.topAreaWidth = this.getUIContext().px2vp(this.windowSize.width) - this.titleButtonRect.width;
           console.info(`titleButtonRect: ${JSON.stringify(this.titleButtonRect)}`);
         } catch (exception) {
           console.error(`Failed to get the area of title buttons. Cause code: ${exception.code}, message: ${exception.message}`);
         }
       }
    
       build() {
         RelativeContainer() {
           Row() {
             Row() {
               Stack() {
                 Row() {
                   Text(this.message)
                 }.width('100%').height('100%')
                 .justifyContent(FlexAlign.Center)
                 .backgroundColor(Color.Pink)
               }.position({ x: 0, y: 0 })
               .width(this.topAreaWidth)
             }
             .position({ x: 0, y: 0 })
             .height(this.topAreaHeight)
             .width('100%')
    
             Row() {
               Button('hide the title bar and avoid the three buttons.')
                 .onClick(() => {
                   if (!this.mainWindow) {
                     return;
                   }
                   this.message = 'Top layout area, hide the title bar and avoid the three buttons.';
                   this.mainWindow.setWindowDecorVisible(false);
                   this.updateTitleButtonRect();
                 })
               Button('do not hide the title bar.')
                 .onClick(() => {
                   if (!this.mainWindow) {
                     return;
                   }
                   this.message = 'Top layout area, do not hide the title bar.';
                   this.mainWindow.setWindowDecorVisible(true);
                   this.topAreaHeight = this.mainWindow.getWindowDecorHeight();
                   this.topAreaWidth = '100%';
                 })
                 .margin(20)
             }.height('50%')
             .width('100%')
             .alignItems(VerticalAlign.Center)
             .justifyContent(FlexAlign.Center)
           }
           .height('100%')
           .width('100%')
         }
         .height('100%')
         .width('100%')
       }
     }

示例代码效果图如下:

下图表示不隐藏标题栏时,应用最顶部的布局区域(粉色区域)只能在标题栏之下。

下图表示隐藏标题栏时,避让窗口三键后,应用最顶部可用于布局的区域(粉色区域)。

自由窗口状态下窗口进入全屏显示

在PC/2in1设备或开启电脑模式的Tablet设备上,点击窗口最大化按钮,窗口默认以最大化显示,且不会自动隐藏标题栏、Dock栏。如果应用需要进入沉浸式全屏显示,隐藏标题栏、Dock栏,则需要进行适配。

典型场景及对应方案如下:

在进入全屏播放后,如果需要退出全屏播放,有以下两种方式:

  • 调用recover()接口将窗口从全屏模式下还原为自由悬浮窗口模式(即窗口模式为window.WindowStatusType.FLOATING),并恢复到进入该模式之前的大小和位置。

  • 调用maximize()接口,传参EXIT_IMMERSIVE,将窗口从全屏改变为最大化状态。