在这里插入图片描述

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀

前言

从 HarmonyOS NEXT 5.0.4(API Level 16)开始,SubWindowOptions 新增了一个 isTopmost 字段。看起来只是一个布尔值,但它背后代表的是子窗口在自由窗口模式下的整个生命周期模型的变化。普通子窗和独立子窗在层级归属、前后台切换、窗口销毁时机上的行为完全不同。这篇文章专门把这个新点拆开来看。

一、先说清楚"普通子窗"有什么问题

在 isTopmost 出现之前,子窗口的生命周期和层级是绑定在主窗或父窗上的。这个设计在全屏或分屏场景下没有问题,但在自由窗口模式(主要是 2in1 设备)下会暴露出几个典型问题:

层级依附:普通子窗的 Z 轴层级始终跟随主窗,主窗被遮挡,子窗也被遮挡。如果子窗是一个工具浮层,用户希望它能"浮"在主窗上方甚至其他窗口上方,普通子窗做不到。

前后台联动:普通子窗切换到后台时,随着主窗一起进入后台。应用如果希望子窗在主窗退到后台后仍能保持独立运行(比如一个悬浮翻译面板、一个持续显示的计时器),普通子窗的模型不适合。

销毁联动:主窗被关闭时,普通子窗会随之销毁。某些场景下开发者希望子窗能有更自主的生命周期,这在之前是做不到的。

isTopmost = true 引入的"独立子窗"就是为了解决这类场景。

二、官方定义的能力边界

根据 HarmonyOS NEXT 5.0.4 发布说明和 @ohos.window API 参考,独立子窗的行为定义如下:

特性普通子窗(isTopmost: false)独立子窗(isTopmost: true)
层叠层级依附于主窗独立层级,不跟随主窗
前后台状态随主窗切换独立的前后台状态
生效条件所有窗口模式仅自由窗口模式下生效
权限要求无ohos.permission.SYSTEM_FLOAT_WINDOW(user_grant)
起始版本API 12API 16

有几点需要特别说明:

  1. 自由窗口模式限制:isTopmost: true 仅在自由窗口状态(Free Window Mode)下才会呈现独立子窗的层级和前后台行为。在全屏、分屏等其他窗口模式下,即使设置了 isTopmost: true,行为等同于普通子窗。

  2. 权限是必要条件:如果应用没有 ohos.permission.SYSTEM_FLOAT_WINDOW 权限,即使代码中设置了 isTopmost: true,创建的也是普通子窗,不会报错但也不会有独立行为。

  3. 适用设备:SYSTEM_FLOAT_WINDOW 权限适用于 Phone、Tablet、2in1,但自由窗口模式目前主要在 2in1 设备上使用。在手机上即使权限获取成功,独立子窗的行为差异也不明显。

  4. isTopmost 属性起始版本是 API 16,createSubWindowWithOptions 本身起始版本是 API 12,两者不同,注意区分。

此外,API 16 同步新增了 Window.setWindowTopmost(isTopmost: boolean): Promise<void>,可以在子窗口创建之后动态切换是否独立。这在某些需要在运行时按需提升子窗层级的场景下有用。

三、搭一个最小示例

目标场景:在一个 2in1 设备的应用里,创建一个独立子窗,让它在主窗切到后台后仍能保持在前台显示。

需要做的事:

  1. module.json5 声明 SYSTEM_FLOAT_WINDOW 权限;
  2. 在 UIAbility 的 onWindowStageCreate 中用 createSubWindowWithOptions 创建子窗,设置 isTopmost: true;
  3. 子窗口加载自定义页面,完成显示。

四、核心代码实现

4.1 配置权限

module.json5 中声明悬浮窗权限,没有这一步,独立子窗能力不会生效。

// entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

SYSTEM_FLOAT_WINDOW 是 user_grant 类型,也就是运行时权限。光在 module.json5 声明还不够,还需要在代码中主动申请。

4.2 运行时申请权限

这段代码解决的问题是:在创建独立子窗之前,确认应用已经拿到悬浮窗权限。

// entry/src/main/ets/entryability/EntryAbility.ets
import { UIAbility, Want, AbilityConstant, bundleManager, common, abilityAccessCtrl } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryAbility extends UIAbility {
  private subWindow: window.Window | null = null;

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 先加载主页面
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error('loadContent failed: ' + JSON.stringify(err));
        return;
      }
      // 主页面加载完成后,申请权限再创建子窗
      this.requestFloatPermissionAndCreateSubWindow(windowStage);
    });
  }

  private requestFloatPermissionAndCreateSubWindow(windowStage: window.WindowStage): void {
    const atManager = abilityAccessCtrl.createAtManager();
    // 申请悬浮窗权限
    atManager.requestPermissionsFromUser(this.context, ['ohos.permission.SYSTEM_FLOAT_WINDOW'])
      .then((result) => {
        const granted = result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
        if (granted) {
          this.createTopmostSubWindow(windowStage);
        } else {
          console.warn('SYSTEM_FLOAT_WINDOW permission denied, subwindow will be a normal one.');
          // 权限被拒绝时,仍可创建,但 isTopmost 行为不生效
        }
      })
      .catch((err: BusinessError) => {
        console.error('requestPermissionsFromUser failed: ' + JSON.stringify(err));
      });
  }

  private createTopmostSubWindow(windowStage: window.WindowStage): void {
    const options: window.SubWindowOptions = {
      title: '独立子窗示例',
      decorEnabled: true,
      isTopmost: true   // API 16+ 独立子窗标志
    };

    windowStage.createSubWindowWithOptions('topmostSubWindow', options)
      .then((subWin) => {
        this.subWindow = subWin;
        // 设置子窗口的尺寸和位置
        subWin.moveWindowTo(200, 200);
        subWin.resize(400, 300);
        // 加载子窗口页面
        subWin.setUIContent('pages/SubWindowPage', (err) => {
          if (err.code) {
            console.error('SubWindow loadContent failed: ' + JSON.stringify(err));
            return;
          }
          subWin.showWindow();
        });
      })
      .catch((err: BusinessError) => {
        console.error('createSubWindowWithOptions failed: ' + JSON.stringify(err));
      });
  }

  onWindowStageDestroy(): void {
    // 主窗销毁时手动关闭子窗,独立子窗也需要显式清理
    if (this.subWindow) {
      this.subWindow.destroyWindow();
      this.subWindow = null;
    }
  }
}

真正需要关注的是这几个地方:

  • isTopmost: true 是核心,但它只在权限已授予且设备处于自由窗口模式时产生独立效果;
  • createSubWindowWithOptions 的调用方是 WindowStage,不是 UIContext。两者都有这个方法,但语义不同,这里在 UIAbility 里用 WindowStage 是更自然的方式;
  • moveWindowTo 和 resize 的调用时机要在 setUIContent 之前,否则在某些情况下位置和尺寸可能不符合预期。

4.3 子窗口页面

// entry/src/main/ets/pages/SubWindowPage.ets
@Entry
@Component
struct SubWindowPage {
  build() {
    Column() {
      Text('我是独立子窗')
        .fontSize(20)
        .fontColor(Color.White)
        .margin({ top: 40 })

      Text('在自由窗口模式下,主窗切后台时我仍在前台')
        .fontSize(14)
        .fontColor('#cccccc')
        .margin({ top: 16 })
        .padding({ left: 20, right: 20 })
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
    .justifyContent(FlexAlign.Start)
  }
}

4.4 动态切换独立状态

如果需要在运行时切换子窗的独立性,可以使用 setWindowTopmost:

// 在已创建的子窗口上动态切换
// 这段代码解决的问题是:按需决定子窗是否升级为独立子窗
if (this.subWindow) {
  this.subWindow.setWindowTopmost(true)
    .then(() => {
      console.info('setWindowTopmost success');
    })
    .catch((err: BusinessError) => {
      console.error('setWindowTopmost failed: ' + JSON.stringify(err));
    });
}

setWindowTopmost 同样需要应用持有 SYSTEM_FLOAT_WINDOW 权限,且同样只在自由窗口状态下产生独立效果。

五、几个关键点拆开看

5.1 isTopmost 和 isModal 的关系

SubWindowOptions 里同时有 isModal(API 13)和 isTopmost(API 16)两个属性。这两者不是同一维度的概念:

  • isModal: true:模态子窗,阻止用户操作父窗口,焦点锁定在子窗;
  • isTopmost: true:独立子窗,不依附父窗的层级和前后台,焦点不受限制;

官方文档没有明确说明这两个属性同时为 true 时的优先级行为。实际开发中建议不要同时设置,除非有明确的场景需要,需要在目标设备上验证实际表现。

5.2 窗口模式检测

独立子窗行为只在自由窗口模式下生效,因此在代码里检测当前窗口模式是有必要的:

import { window } from '@kit.ArkUI';

// 获取主窗并判断是否处于自由窗口模式
const mainWindow = await windowStage.getMainWindow();
const windowProperties = mainWindow.getWindowProperties();

if (windowProperties.windowRect && mainWindow.getWindowStatus() === window.WindowStatusType.FLOATING) {
  // 当前是自由窗口状态,isTopmost 将生效
}

window.WindowStatusType.FLOATING 对应自由窗口状态,是判断 isTopmost 是否会生效的依据。

5.3 decorEnabled 与 title 的配合

独立子窗在自由窗口模式下有独立的标题栏,如果 decorEnabled: false,子窗口没有标题栏,用户可能无法通过拖拽移动窗口。建议独立子窗场景下将 decorEnabled 设为 true,否则用户体验不完整。

5.4 WindowStage vs UIContext 创建子窗

两个入口都有 createSubWindowWithOptions:

  • WindowStage.createSubWindowWithOptions:在 UIAbility 的生命周期中调用,适合在应用初始化阶段创建;
  • UIContext.createSubWindowWithOptions(起始版本 12):在页面的 UI 上下文中调用,适合在特定页面交互时创建。

在 UIAbility 中直接管理子窗的完整生命周期,用 WindowStage 更直接;如果子窗的创建逻辑在某个具体页面里触发,用 UIContext 更合适。两者的 isTopmost 行为是一致的。

六、容易踩坑的地方

一、只在 module.json5 声明了权限,没有运行时申请

SYSTEM_FLOAT_WINDOW 是 user_grant 权限,仅在配置文件里声明是不够的。没有通过 requestPermissionsFromUser 获得用户授权,isTopmost 不会报错,但独立子窗行为不会生效,子窗依然表现为普通子窗。这个问题比较隐蔽,因为代码不会抛出异常。

二、在全屏模式下测试,误判独立子窗没有效果

isTopmost: true 在全屏和分屏模式下没有独立行为,这不是 bug,是设计如此。必须在 2in1 设备上以自由窗口模式运行才能观察到独立层级和独立前后台状态。如果测试设备是手机且一直在全屏运行,会看不到区别。

三、忘记显式销毁独立子窗

普通子窗随主窗销毁,独立子窗因为有独立生命周期,主窗销毁时可能不会自动清理。需要在 onWindowStageDestroy 中显式调用 subWindow.destroyWindow(),防止窗口资源泄漏。

四、isTopmost 是 API 16 的字段,在低版本 SDK 编译会报错

如果项目的 compileSdkVersion 低于 16,直接使用 isTopmost 会在编译阶段报属性不存在。需要确认 build-profile.json5 中的 compileSdkVersion 和 minAPIVersion 配置。

// build-profile.json5
{
  "app": {
    "compileSdkVersion": 16,
    "compatibleSdkVersion": 12
  }
}

如果要兼容 API 12 及以上的设备,可以用版本判断来按条件使用 isTopmost:

const options: window.SubWindowOptions = {
  title: '子窗口',
  decorEnabled: true,
};

// isTopmost 字段在 API 16 引入,低版本不存在
// 如果 compileSdkVersion >= 16,可直接添加;否则需运行时版本判断
if (deviceInfo.sdkApiVersion >= 16) {
  (options as window.SubWindowOptions & { isTopmost?: boolean }).isTopmost = true;
}

七、排查顺序

遇到独立子窗行为不符合预期时,建议按这个顺序排查:

  1. 确认 API Level:检查 build-profile.json5,compileSdkVersion 是否 >= 16;
  2. 确认设备和窗口模式:测试是否在 2in1 设备上以自由窗口模式运行;
  3. 确认权限授予状态:运行时是否弹出了权限申请弹窗,用户是否点击了"允许";
  4. 确认 isTopmost 字段是否写在了正确的 SubWindowOptions 对象里:不是 WindowOptions,是 SubWindowOptions;
  5. 确认子窗口是否成功创建:检查 createSubWindowWithOptions 的 Promise 有没有进入 catch 分支;
  6. 确认是否显式调用了 showWindow():子窗口创建后默认不显示,需要主动 show;
  7. 最后检查业务逻辑:观察主窗切后台时,子窗口是否仍然可见,层级是否正确。

开发经验总结

  • isTopmost 是 SubWindowOptions 在 API 16 引入的新字段,用于创建"独立子窗",核心差异是独立的层叠层级和独立的前后台状态,仅在自由窗口模式下生效。
  • SYSTEM_FLOAT_WINDOW 是 user_grant 权限,必须运行时主动申请,仅声明不生效,且这个问题没有运行时报错。
  • setWindowTopmost 提供了动态调整的能力,适合需要在交互过程中按需切换子窗独立性的场景。
  • 独立子窗需要显式销毁,不能依赖主窗销毁时的联动清理。
  • 这个能力目前最有意义的使用场景是 2in1 设备上的悬浮工具窗,在手机全屏场景下和普通子窗行为无差异。

如果你的应用有在自由窗口模式下运行的计划,可以观察一下当前子窗口在主窗和其他窗口之间相互遮挡时的层级表现——这往往是判断是否需要用独立子窗的最直接依据。

❤️ 如果本文帮到了你…

  • 请点个赞,让我知道你还在坚持阅读技术长文!
  • 请收藏本文,因为你以后一定还会用上!
  • 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
Logo

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

更多推荐