HarmonyOS《柚兔学伴》项目实战04-沉浸式窗口与断点系统
沉浸式窗口与断点系统
沉浸式体验是现代移动应用的重要设计目标——内容从状态栏底部延伸到导航栏,实现全屏视觉冲击。同时,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);
xs 和 xl 为可选参数,缺省时分别回退到 sm 和 lg,简化了配置。
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 组件只需声明式绑定即可实现自适应布局,无需手动处理设备差异。
更多推荐


所有评论(0)