目录

应用声明支持智慧多窗

声明支持悬浮窗

声明支持分屏

应用内分屏

应用布局适配智慧多窗

悬浮窗的比例

分屏的比例

应用布局适配智慧多窗的方案

顶部窗口控制条避让适配智慧多窗


应用声明支持智慧多窗

当应用需要智慧多窗的能力时,可以通过在module.json5配置文件中对应标签添加相关字段声明支持。

声明支持悬浮窗

开发者可以通过在module.json5配置文件中abilities标签下的supportWindowMode属性增加“floating”字段或使用缺省值以声明应用支持悬浮窗。

说明

supportWindowMode缺省值为["fullscreen", "split", "floating"]。

supportWindowMode属性主要标识当前UIAbility所支持的窗口模式,支持的字段及含义如下表所示。

字段

说明

fullscreen

窗口支持全屏显示。

split

窗口支持分屏显示。

floating

窗口支持悬浮窗显示。

在应用声明支持智慧多窗后,还可根据业务场景的需要配置是否支持横向悬浮窗或上下分屏模式。

当应用需要支持横向悬浮窗时,开发者可以通过在module.json5配置文件中abilities标签下的preferMultiWindowOrientation属性增加“landscape”或者“landscape_auto”配合API以声明应用支持横向悬浮窗或上下分屏模式。

preferMultiWindowOrientation属性主要标识当前UIAbility组件多窗布局方向,支持的字段及含义如下表所示。

配置值

说明

效果

portrait

多窗布局方向为竖向。建议竖向游戏类应用配置。

手机

手势触发悬浮窗:竖向悬浮窗

手势触发分屏:不支持

分屏样式切换:不涉及

折叠屏手机展开态

手势触发悬浮窗:竖向悬浮窗

手势触发分屏:形成左右分屏

分屏样式切换:不支持样式切换

landscape

多窗布局方向为横向,配置后支持横向悬浮窗和上下分屏。建议横向游戏类应用配置。

手机

手势触发悬浮窗:横向悬浮窗

手势触发分屏:不支持

分屏样式切换:不涉及

折叠屏手机展开态

手势触发悬浮窗:横向悬浮窗

手势触发分屏:形成上下分屏

分屏样式切换:不支持样式切换

landscape_auto

多窗布局动态可变为横向,需要配合API(enableLandscapeMultiWindow / disableLandscapeMultiWindow)使用。建议视频类应用配置。

系统识别应用为横向全屏播放

手机

手势触发悬浮窗:横向悬浮窗

手势触发分屏:形成上下分屏

分屏样式切换:不涉及

折叠屏手机展开态

手势触发悬浮窗:横向悬浮窗

手势触发分屏:形成上下分屏

分屏样式切换:支持样式切换

系统识别应用为非横向全屏播放:同配置为default

default

缺省值,参数不配置时默认为default。

建议其他应用类配置。

折叠屏手机折叠态 & 手机

手势触发悬浮窗:竖向悬浮窗

手势触发分屏:形成上下分屏

分屏样式切换:不涉及

折叠屏手机展开态

手势触发悬浮窗:竖向悬浮窗

手势触发分屏:形成左右分屏

分屏样式切换:支持样式切换

声明支持分屏

开发者可以通过在module.json5配置文件中abilities标签下的supportWindowMode属性增加“split”字段或使用缺省值以声明应用支持分屏。

说明

supportWindowMode缺省值为["fullscreen", "split", "floating"]。

supportWindowMode属性主要标识当前UIAbility所支持的窗口模式,支持的字段及含义如下表所示。

字段

说明

fullscreen

窗口支持全屏显示。

split

窗口支持分屏显示。

floating

窗口支持悬浮窗显示。

应用内分屏

应用内分屏功能允许声明支持分屏的应用在全屏显示模式下,通过调用startAbility方法启动UIAbility并形成分屏。该功能能够增强应用的多任务处理能力,提升用户的操作体验。

此处以点击按钮启动分屏为例,主要步骤和示例如下所示:

  1. 在应用中获取UIAbilityContext 对象,这是启动分屏所必需的上下文对象,用于后续调用startAbility接口。
    let context = getContext(this) as common.UIAbilityContext;
  2. 调用startAbility接口启动UIAbility,形成分屏。调用startAbility接口时,设置StartOptions对象,需要指定窗口模式windowMode(需设置为WINDOW_MODE_SPLIT_PRIMARY或者WINDOW_MODE_SPLIT_SECONDARY),并可根据需要设置其他StartOptions属性或startAbility参数,如Want对象。
    // 创建StartOptions并设置为主窗口模式
    let option: StartOptions = { windowMode: AbilityConstant.WindowMode.WINDOW_MODE_SPLIT_PRIMARY }; 
    let want: Want = { bundleName: 'com.example.startsplitdemo', abilityName: 'EntryAbility1', moduleName: '' };
    context.startAbility(want, option);
    
  3. 若继续执行上述步骤,可继续启动其他UIAbility窗口,呈现左右分屏或替换一侧的分屏窗口。

完整示例如下:

使用DevEco Studio新建Ability,创建EntryAbility1和EntryAbility2,对应文中组成分屏的两个窗口页面,加载页面为默认页面Index.ets。

// Index.ets
import { AbilityConstant, common, StartOptions, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct Index {
  @State name: string = '';

  aboutToAppear(): void {
    let context = getContext(this) as common.UIAbilityContext;
    this.name = context.abilityInfo.name;
  }

  build() {
    Column({ space: 20 }) {
      Text(this.name)

      Button() {
        Text('启动应用内分屏')
      }
      .height(40)
      .onClick(() => {
        let context = getContext(this) as common.UIAbilityContext;
        let want: Want = { bundleName: 'com.example.startsplitdemo', abilityName: 'EntryAbility1', moduleName: '' };
        // 创建StartOptions并设置窗口模式为分屏模式,左侧分屏
        let option: StartOptions = { windowMode: AbilityConstant.WindowMode.WINDOW_MODE_SPLIT_PRIMARY };
        try {
          context.startAbility(want, option, (error) => {
            if (error.code) {
              return;
            }
            hilog.info(0x0000, 'testTag', '启动分屏成功');
          });
        } catch (paramError) {
        }
      })

      Button() {
        Text('启动另一分屏窗口')
      }
      .height(40)
      .onClick(() => {
        let context = getContext(this) as common.UIAbilityContext;
        let want: Want = { bundleName: 'com.example.startsplitdemo', abilityName: 'EntryAbility2', moduleName: '' };
        // 指定启动EntryAbility2的窗口模式,右侧分屏
        let option: StartOptions = { windowMode: AbilityConstant.WindowMode.WINDOW_MODE_SPLIT_SECONDARY };
        context.startAbility(want, option);
      })
    }
    .padding(20)
    .height('100%')
    .width('100%')
  }
}

应用布局适配智慧多窗介绍

由于应用从全屏进入智慧多窗(悬浮窗/分屏)模式后,窗口尺寸、宽高比例会发生变化,所以需要开发者适配应用窗口在不同尺寸、不同比例下的自适应布局,以确保应用窗口在各种形态下都能呈现出最佳的视觉效果,提供更好的用户体验

应用布局适配智慧多窗

悬浮窗的比例

不同设备支持悬浮窗的比例如下所示:

设备

竖向悬浮窗宽高比

横向悬浮窗宽高比

手机

3:4.575

16:9

折叠屏手机展开态

9:16

16:9

说明

  • 顶部窗口控制条的避让区域不包含在应用布局区域内,窗口高度去除避让区域的32vp为应用布局区域的高度。
  • 手机:悬浮窗模式下,应用窗口真实宽度为屏幕宽度。竖向时,高度根据宽高比3 : 4.575动态调整;横向时,高度根据宽高比16 : 9动态调整(该比例超出屏幕时,以当前全屏屏幕比例计算)。
  • 折叠屏手机展开态:悬浮窗模式下,应用窗口真实宽度为折叠屏手机折叠态时的屏幕宽度。竖向时,高度根据宽高比9 : 16动态调整;横向时,高度根据宽高比16 : 9动态调整。

分屏的比例

目前支持两种分屏样式:“上下分屏”和“左右分屏”。

分屏比例指的是分屏下两应用间尺寸的比例,调整分屏比例会调整应用窗口的大小。

默认形成分屏后分屏比例为1:1,拖动中间的分屏条可以改变分屏比例档位。手机“上下分屏”可调节档位1:2、1:1、2:1,“左右分屏”可调节档位为1:1。手机折叠屏展开态可调节档位只有1:1。

设备

默认分屏比例

分屏可调节档位

手机

1:1

“上下分屏”: 1:1, 1:2, 2:1

“左右分屏”: 1:1

手机折叠屏展开态

1:1

“上下分屏”和 “左右分屏”: 1:1

应用布局可以通过自适应布局和响应式布局来更新自身布局,避免出现截断、挤压、堆叠等现象。

应用布局适配智慧多窗的方案

无论是悬浮窗还是分屏,当应用进入智慧多窗模式时,应用的窗口尺寸发生变化,所以应用需要根据不同的窗口尺寸调整自身布局。

主要可以通过窗口的on('windowSizeChange')方法实现对窗口尺寸大小变化的监听。再根据窗口的尺寸变化,更新调整自身应用布局以实现适配。

主要步骤和示例如下:

  1. 在onWindowStageCreate方法中,获取Window对象。
  2. 通过getWindowProperties方法返回值中的windowRect获取窗口尺寸,写入AppStorage中用于UI侧窗口尺寸的首次初始化赋值。
  3. 使用on('windowSizeChange')注册窗口尺寸变化时的监听,并写入AppStorage中供UI侧布局使用。
  4. UI侧通过@StorageLink绑定窗口尺寸后,AppStorage中属性key值对应的数据一旦改变,UI侧会同步修改。
  5. @StorageLink装饰的数据本身是状态变量,所以窗口尺寸发生变化时,会引起组件的重新渲染,开发者可以根据最新的窗口尺寸动态调整应用布局。
    // EntryAbility.ets
    import { UIAbility } from '@kit.AbilityKit';
    import { window } from '@kit.ArkUI';
    
    export default class EntryAbility extends UIAbility {
      onWindowStageCreate(windowStage: window.WindowStage): void {
        console.info('Ability onWindowStageCreate.');
        windowStage.getMainWindow().then((windowClass) => {
          // 获取窗口尺寸,存入AppStorage
          AppStorage.setOrCreate('winWidth', windowClass.getWindowProperties().windowRect.width);
          AppStorage.setOrCreate('winHeight', windowClass.getWindowProperties().windowRect.height);
          // 监听窗口尺寸变化
          windowClass.on('windowSizeChange', (windowSize) => {
            AppStorage.setOrCreate('winWidth', windowSize.width);
            AppStorage.setOrCreate('winHeight', windowSize.height);
          });
        });
        windowStage.loadContent('pages/Index', (err) => {
          if (err.code) {
            console.error('Failed to load the content. Cause: ' + JSON.stringify(err));
            return;
          }
          console.info('Succeeded in loading the content.');
        });
      }
    }
    
    // Index.ets
    @Entry
    @Component
    struct Index {
      // 初始化参数,这里会初始化为AppStorage中存储的值
      @StorageLink('winWidth') winWidth: number = 1260;
      @StorageLink('winHeight') winHeight: number = 2224;
    
      aboutToAppear() {
        console.info('Current window size. width: ' + this.winWidth + ', height: ' + this.winHeight);
      }
    
      build() {
        Row() {
          // 根据winWidth、winHeight动态调整应用布局
          // ...
        }
        .size({
          width: px2vp(this.winWidth),
          height: px2vp(this.winHeight)
        })
      }
    }
    

顶部窗口控制条避让适配智慧多窗

顶部窗口控制条是应用窗口处于智慧多窗模式下,应用顶部的操作横条 

 。

顶部窗口控制条示意图如下所示:

顶部横条的避让可通过以下两种方式适配:

  • 使用窗口的避让能力:通过setWindowLayoutFullScreen设置窗口布局是否为沉浸式布局。

    沉浸式布局是指应用布局不避让状态栏、导航栏以及智慧多窗顶部横条,这可能发生组件与顶部横条的重叠,导致文字遮挡、点击事件冲突等情况。非沉浸式布局是指布局避让状态栏、导航栏以及智慧多窗顶部横条,组件不会与其重叠。因此可设置isLayoutFullScreen值为false使窗口的布局为非沉浸式布局。

    示例:

  • // Index.ets
    import { BusinessError } from '@kit.BasicServicesKit';
    import { window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct Index {
      @State message: string = '非沉浸式布局';
      private windowClass: window.Window | undefined = undefined;
    
      aboutToAppear(): void {
        try {
          window.getLastWindow(getContext(this), (err: BusinessError, data) => {
            const errCode: number = err.code;
            if (errCode) {
              console.error('Failed to obtain the top window. Cause: ' + JSON.stringify(err));
              return;
            }
            this.windowClass = data;
            console.info('Succeeded in obtaining the top window. Data: ' + JSON.stringify(data));
          });
        } catch (exception) {
          console.error('Failed to obtain the top window. Cause: ' + JSON.stringify(exception));
        }
      }
    
      private setWindowLayoutFullScreen(isLayoutFullScreen: boolean) {
        if (!this.windowClass) {
          return;
        }
        try {
          this.windowClass.setWindowLayoutFullScreen(isLayoutFullScreen, (err: BusinessError) => {
            const errCode: number = err.code;
            if (errCode) {
              console.error('Failed to set the window layout to full-screen mode. Cause:' + JSON.stringify(err));
              return;
            }
            console.info('Succeeded in setting the window layout to full-screen mode.');
          });
        } catch (exception) {
          console.error('Failed to set the window layout to full-screen mode. Cause:' + JSON.stringify(exception));
        }
      }
    
      build() {
        Stack({ alignContent: Alignment.TopStart }) {
          Column() {
            Text(this.message)
              .fontSize(25)
              .fontWeight(FontWeight.Bold)
              .margin({
                top: '2%',
                bottom: '40%'
              })
    
            Button() {
              Text('设置窗口为沉浸式布局')
                .fontSize(18)
                .fontWeight(FontWeight.Normal)
            }
            .type(ButtonType.Normal)
            .borderRadius(15)
            .margin({ top: 20 })
            .stateStyles({
              normal: {
                .backgroundColor('#ff6b89d4')
              },
              pressed: {
                .backgroundColor('#ffc81f2a')
              }
            })
            .width('60%')
            .height('6%')
            .onClick(() => {
              this.setWindowLayoutFullScreen(true);
              this.message = '沉浸式布局';
            })
    
            Button() {
              Text('设置窗口为非沉浸式布局')
                .fontSize(18)
                .fontWeight(FontWeight.Normal)
            }
            .type(ButtonType.Normal)
            .borderRadius(15)
            .margin({ top: 20 })
            .stateStyles({
              normal: {
                .backgroundColor('#ff6b89d4')
              },
              pressed: {
                .backgroundColor('#ffc81f2a')
              }
            })
            .width('60%')
            .height('6%')
            .onClick(() => {
              this.setWindowLayoutFullScreen(false);
              this.message = '非沉浸式布局';
            })
          }
          .width('100%')
        }
        .backgroundColor('#fceaeaea')
        .height('100%')
      }
    }
    

    应用主动避让:应用不使用窗口避让能力(即设置窗口为沉浸式布局)。首次通过getWindowAvoidArea接口可获取屏幕顶部需要规避的矩阵区域topRect,获取到该值后应用可对应做布局避让,并且注册on('avoidAreaChange')监听系统避让区域变化以进行布局的动态调整。

  • // Index.ets
    import { BusinessError } from '@kit.BasicServicesKit';
    import { window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct Index {
      @State topSafeHeight: number = 0;
    
      aboutToAppear(): void {
        try {
          let windowClass: window.Window | undefined = undefined;
          window.getLastWindow(getContext(this), (err: BusinessError, data) => {
            const errCode: number = err.code;
            if (errCode) {
              console.error('Failed to obtain the top window. Cause: ' + JSON.stringify(err));
              return;
            }
            windowClass = data;
            windowClass.setWindowLayoutFullScreen(true);
            this.topSafeHeight = px2vp(windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height);
            windowClass.on('avoidAreaChange', (data) => {
              if (data.type == window.AvoidAreaType.TYPE_SYSTEM) {
                this.topSafeHeight = px2vp(data.area.topRect.height)
              }
            })
            console.info('Succeeded in obtaining the top window. Data: ' + JSON.stringify(data));
          });
        } catch (exception) {
          console.error('Failed to obtain the top window. Cause: ' + JSON.stringify(exception));
        }
      }
    
      build() {
        Stack({ alignContent: Alignment.TopStart }) {
          // 顶部避让区域
          Row() {
          }
          .height(this.topSafeHeight)
          .width("100%")
    
          // 根据topSafeHeight动态调整应用布局
          // ...
        }
        .height('100%')
      }
    }
    

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐