沉浸式窗口与断点系统

沉浸式体验是现代移动应用的重要设计目标——内容从状态栏底部延伸到导航栏,实现全屏视觉冲击。同时,HarmonyOS 支持手机、平板、2in1 等多种设备形态,应用需要根据屏幕尺寸自适应布局。柚兔学伴通过 WindowUtil 实现沉浸式窗口管理,通过 BreakpointSystem 实现多设备断点适配,两者协同构建了完整的屏幕适配方案。

一、沉浸式窗口实现

请添加图片描述

1.1 全屏布局

沉浸式窗口的核心是将内容延伸到系统栏区域。在 WindowUtil.requestFullScreen 中:

// common/src/main/ets/util/WindowUtil.ets
public static requestFullScreen(windowStage: window.WindowStage, context: Context): void {
  windowStage.getMainWindow((err: BusinessError, data: window.Window) => {
    if (err.code) {
      Logger.error(TAG, `Failed to obtain the main window. Cause: ${err.code}`);
      return;
    }
    const windowClass: window.Window = data;
    try {
      // HPR 设备特殊处理:限制窗口尺寸
      if (deviceInfo.productSeries === ProductSeriesEnum.HPR) {
        WindowUtil.resetWindowSize(windowClass);
      }
      // 设置全屏布局——内容延伸到状态栏和导航栏区域
      const promise: Promise<void> = windowClass.setWindowLayoutFullScreen(true);
      promise.then(() => {
        Logger.info(TAG, 'Succeeded in setting the window layout to full-screen mode.');
      }).catch((err: BusinessError) => {
        Logger.error(TAG, `Failed to set the window layout to full-screen mode.`);
      });
      // 获取设备尺寸信息
      WindowUtil.getDeviceSize(context);
    } catch {
      Logger.error(TAG, 'Failed to set the window layout to full-screen mode.');
    }
  });
}

setWindowLayoutFullScreen(true) 使窗口内容布局覆盖整个屏幕,包括状态栏和导航指示条区域。但此时系统栏仍然可见,只是内容可以延伸到其下方。

1.2 隐藏标题栏

public static hideTitleBar(windowStage: window.WindowStage) {
  windowStage.getMainWindow().then((data: window.Window) => {
    try {
      if (canIUse('SystemCapability.Window.SessionManager')) {
        data.setWindowDecorVisible(false);         // 隐藏窗口装饰
        data.setWindowDecorHeight(CommonConstants.NAVIGATION_HEIGHT); // 设置装饰高度
      }
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      Logger.error(TAG, `Failed to set the visibility of window decor.`);
    }
  });
}

setWindowDecorVisible(false) 隐藏系统标题栏,使应用完全掌控顶部区域。

1.3 状态栏颜色控制

沉浸式布局下,需要根据内容背景调整状态栏图标颜色,确保可见性:

public static updateStatusBarColor(context: common.BaseContext, isDark: boolean): void {
  window.getLastWindow(context).then((windowClass: window.Window) => {
    try {
      windowClass.setWindowSystemBarProperties({
        statusBarContentColor: isDark ? StatusBarColorType.WHITE : StatusBarColorType.BLACK
      }).then(() => {
        Logger.info(TAG, 'Succeeded in setting the system bar properties.');
      });
    } catch (error) {
      Logger.error(TAG, `Failed to set the system bar properties.`);
    }
  });
}

// 状态栏颜色枚举
export enum StatusBarColorType {
  WHITE = '#ffffff',       // 深色背景使用白色图标
  BLACK = '#E5000000',     // 浅色背景使用黑色图标(90%透明度黑)
}

当页面背景为深色时传入 isDark: true,状态栏图标变白;浅色背景则变黑。

二、避让区域管理

全屏布局后,内容可能与状态栏和底部导航指示条重叠,需要获取避让区域高度并预留空间。

2.1 获取避让区域

public static registerBreakPoint(windowStage: window.WindowStage) {
  windowStage.getMainWindow((err: BusinessError, data: window.Window) => {
    if (err.code) {
      Logger.error(TAG, `Failed to get main window: ${err.message}`);
      return;
    }
    try {
      // 初始化断点系统
      BreakpointSystem.getInstance().updateWidthBp(data);

      const globalInfoModel: GlobalInfoModel =
        AppStorage.get('GlobalInfoModel') || new GlobalInfoModel();

      // 获取状态栏避让区域
      const systemAvoidArea: window.AvoidArea =
        data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      globalInfoModel.statusBarHeight = px2vp(systemAvoidArea.topRect.height);

      // 获取底部导航指示条避让区域
      const bottomArea: window.AvoidArea =
        data.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      globalInfoModel.naviIndicatorHeight = px2vp(bottomArea.bottomRect.height);

      AppStorage.setOrCreate('GlobalInfoModel', globalInfoModel);

      // 监听窗口尺寸变化
      data.on('windowSizeChange', () => WindowUtil.onWindowSizeChange(data));
      // 监听避让区域变化
      data.on('avoidAreaChange', (avoidAreaOption) => {
        if (avoidAreaOption.type === window.AvoidAreaType.TYPE_SYSTEM ||
            avoidAreaOption.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
          WindowUtil.setAvoidArea(avoidAreaOption.type, avoidAreaOption.area);
        }
      });
    } catch (e) {
      Logger.error(TAG, `getWindowAvoidArea failed.`);
    }
  });
}

两种避让区域类型:

  • TYPE_SYSTEM:顶部状态栏区域,topRect.height 即状态栏高度
  • TYPE_NAVIGATION_INDICATOR:底部导航指示条区域,bottomRect.height 即指示条高度

2.2 避让区域动态更新

当系统栏状态变化(如横竖屏切换、折叠屏展开)时,通过 avoidAreaChange 监听器实时更新:

public static setAvoidArea(type: window.AvoidAreaType, area: window.AvoidArea) {
  const globalInfoModel: GlobalInfoModel =
    AppStorage.get('GlobalInfoModel') || new GlobalInfoModel();
  if (type === window.AvoidAreaType.TYPE_SYSTEM) {
    globalInfoModel.statusBarHeight = px2vp(area.topRect.height);
  } else {
    globalInfoModel.naviIndicatorHeight = px2vp(area.bottomRect.height);
  }
  AppStorage.setOrCreate('GlobalInfoModel', globalInfoModel);
}

2.3 GlobalInfoModel 全局屏幕信息

所有屏幕相关信息统一存储在 GlobalInfoModel 中,通过 AppStorage 全局共享:

// common/src/main/ets/model/GlobalInfoModel.ets
@Observed
export class GlobalInfoModel {
  public foldExpanded: boolean = false;           // 折叠屏是否展开
  public currentBreakpoint: BreakpointTypeEnum = BreakpointTypeEnum.MD;  // 当前断点
  public naviIndicatorHeight: number = 0;         // 底部指示条高度(vp)
  public statusBarHeight: number = 0;             // 状态栏高度(vp)
  public decorHeight: number = 0;                 // 窗口装饰高度
  public deviceHeight: number = 0;                // 设备高度(vp)
  public deviceWidth: number = 0;                 // 设备宽度(vp)
}

在 UI 组件中使用 @StorageLink 响应式绑定:

@StorageLink('GlobalInfoModel') globalInfo: GlobalInfoModel = new GlobalInfoModel();

// 为内容区域添加顶部 padding,避开状态栏
.padding({ top: this.globalInfo.statusBarHeight })
// 为底部添加 padding,避开导航指示条
.padding({ bottom: this.globalInfo.naviIndicatorHeight })

三、断点系统

3.1 断点定义

BreakpointSystem 根据窗口宽度将设备划分为五个断点等级:

断点 宽度范围 典型设备
XS < 320vp 小屏穿戴设备
SM 320vp - 600vp 手机竖屏
MD 600vp - 840vp 手机横屏/小平板
LG 840vp - 1440vp 平板/折叠屏
XL > 1440vp 大屏/2in1 设备
export enum BreakpointTypeEnum {
  XS = 'xs',
  SM = 'sm',
  MD = 'md',
  LG = 'lg',
  XL = 'xl',
}

3.2 BreakpointSystem 单例

// common/src/main/ets/util/BreakpointSystem.ets
export class BreakpointSystem {
  private static instance: BreakpointSystem;
  private currentBreakpoint: BreakpointTypeEnum = BreakpointTypeEnum.MD;

  private constructor() {}

  public static getInstance(): BreakpointSystem {
    if (!BreakpointSystem.instance) {
      BreakpointSystem.instance = new BreakpointSystem();
    }
    return BreakpointSystem.instance;
  }

  public updateWidthBp(window: window.Window): void {
    try {
      const mainWindow: window.WindowProperties = window.getWindowProperties();
      const windowWidth: number = mainWindow.windowRect.width;
      const windowWidthVp = px2vp(windowWidth);    // 像素转 VP

      // HPR 设备直接判定为 XL 断点
      const deviceType = deviceInfo.productSeries;
      if (deviceType === ProductSeriesEnum.HPR) {
        this.updateCurrentBreakpoint(BreakpointTypeEnum.XL);
        return;
      }

      // 根据窗口宽度 VP 判定断点
      let widthBp: BreakpointTypeEnum = BreakpointTypeEnum.MD;
      if (windowWidthVp < 320) {
        widthBp = BreakpointTypeEnum.XS;
      } else if (windowWidthVp >= 320 && windowWidthVp < 600) {
        widthBp = BreakpointTypeEnum.SM;
      } else if (windowWidthVp >= 600 && windowWidthVp < 840) {
        widthBp = BreakpointTypeEnum.MD;
      } else if (windowWidthVp >= 840 && windowWidthVp < 1440) {
        widthBp = BreakpointTypeEnum.LG;
      } else {
        widthBp = BreakpointTypeEnum.XL;
      }
      this.updateCurrentBreakpoint(widthBp);
    } catch (error) {
      Logger.error(TAG, `Failed to getWindowProperties.`);
    }
  }

  public updateCurrentBreakpoint(breakpoint: BreakpointTypeEnum): void {
    if (this.currentBreakpoint !== breakpoint) {
      this.currentBreakpoint = breakpoint;
      const globalInfoModel: GlobalInfoModel =
        AppStorage.get('GlobalInfoModel') || new GlobalInfoModel();
      globalInfoModel.currentBreakpoint = this.currentBreakpoint;
      AppStorage.setOrCreate('GlobalInfoModel', globalInfoModel);
    }
  }
}

关键点:

  • 使用 px2vp() 将像素宽度转换为 VP,保证不同像素密度的设备断点一致
  • HPR(华为 PC 类设备)直接判定为 XL 断点
  • 断点变化时更新 GlobalInfoModel.currentBreakpoint,触发 UI 刷新

3.3 BreakpointType 泛型类

BreakpointType<T> 是断点系统的核心工具类,可以根据当前断点返回不同的值(如布局参数、字体大小、组件实例):

export interface BreakpointTypes<T> {
  xs?: T;
  sm: T;
  md: T;
  lg: T;
  xl?: T;
}

export class BreakpointType<T> {
  private xs: T;
  private sm: T;
  private md: T;
  private lg: T;
  private xl: T;

  public constructor(param: BreakpointTypes<T>) {
    this.xs = param.xs ?? param.sm;    // xs 缺省取 sm 值
    this.sm = param.sm;
    this.md = param.md;
    this.lg = param.lg;
    this.xl = param.xl ?? param.lg;    // xl 缺省取 lg 值
  }

  public getValue(currentBreakpoint: string): T {
    if (currentBreakpoint === BreakpointTypeEnum.XS) return this.xs;
    if (currentBreakpoint === BreakpointTypeEnum.SM) return this.sm;
    if (currentBreakpoint === BreakpointTypeEnum.MD) return this.md;
    if (currentBreakpoint === BreakpointTypeEnum.XL) return this.xl;
    return this.lg;
  }
}

使用示例——根据断点设置不同的列数和间距:

const columns = new BreakpointType<number>({
  sm: 2,    // 手机两列
  md: 3,    // 横屏三列
  lg: 4,    // 平板四列
});

const gridCol = columns.getValue(this.globalInfo.currentBreakpoint);

xsxl 为可选参数,缺省时分别回退到 smlg,简化了配置。

3.4 窗口尺寸变化监听

当窗口尺寸变化(旋转屏幕、折叠屏开合、自由窗口拖拽)时,触发断点重新计算:

public static onWindowSizeChange(window: window.Window) {
  WindowUtil.getDeviceSize(getContext());                // 更新设备尺寸
  BreakpointSystem.getInstance().onWindowSizeChange(window); // 更新断点
}

四、HPR 设备特殊处理

对于 HPR(HarmonyOS PC 类设备),柚兔学伴做了特殊适配:

private static resetWindowSize(windowClass: window.Window): void {
  if (canIUse('SystemCapability.Window.SessionManager')) {
    const windowSize: display.Display = display.getDefaultDisplaySync();
    const appWidth: number = windowSize.width * 9 / 10;   // 90% 屏幕宽度
    const appHeight: number = windowSize.height * 7 / 8;  // 87.5% 屏幕高度
    const windowLimits: window.WindowLimits = {
      maxWidth: appWidth, maxHeight: appHeight,
      minWidth: appWidth, minHeight: appHeight,
    };
    windowClass.setWindowLimits(windowLimits);
    windowClass.moveWindowToAsync(windowSize.width / 20, windowSize.height / 16);
  }
}

HPR 设备上不使用全屏窗口,而是限制为屏幕 90%×87.5% 的固定窗口,模拟 PC 应用的窗口体验。

五、完整适配流程

应用启动 → onWindowStageCreate
  │
  ├── WindowUtil.requestFullScreen()     ← 设置全屏布局
  │     └── WindowUtil.getDeviceSize()   ← 读取设备宽高
  │
  ├── WindowUtil.registerBreakPoint()    ← 注册断点与避让
  │     ├── BreakpointSystem.updateWidthBp()   ← 计算初始断点
  │     ├── getWindowAvoidArea(TYPE_SYSTEM)    ← 读取状态栏高度
  │     ├── getWindowAvoidArea(TYPE_NAVIGATION_INDICATOR) ← 读取指示条高度
  │     ├── on('windowSizeChange')             ← 监听窗口变化
  │     └── on('avoidAreaChange')              ← 监听避让变化
  │
  └── AppStorage.setOrCreate('GlobalInfoModel') ← 全局共享
        │
        UI 组件通过 @StorageLink 响应式读取
        ├── statusBarHeight → 顶部 padding
        ├── naviIndicatorHeight → 底部 padding
        ├── currentBreakpoint → BreakpointType.getValue() → 自适应布局
        └── deviceWidth / deviceHeight → 条件渲染

小结

柚兔学伴通过 WindowUtil 实现沉浸式窗口管理——全屏布局、隐藏标题栏、状态栏颜色适配、避让区域获取与动态更新;通过 BreakpointSystem 实现多设备断点适配——基于窗口宽度 VP 划分五个断点等级,提供 BreakpointType<T> 泛型类简化断点值映射。两者通过 GlobalInfoModel + AppStorage 统一管理屏幕状态,UI 组件只需声明式绑定即可实现自适应布局,无需手动处理设备差异。

Logo

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

更多推荐