【HarmonyOS实战】 沉浸式全屏布局:状态栏和导航栏那些坑
文章目录前言一、什么是沉浸式布局二、开启沉浸式的代码三、获取安全区域高度3.1 AvoidAreaType 有哪些类型3.2 avoidArea 的结构四、动态监听安全区域变化五、在页面中消费安全区域高度5.1 换算单位px → vp六、地图页的实际效果标题栏的位置处理七、主页的状态栏适配八、不同机型的差异总结前言现在几乎所有优秀的移动 App 都是沉浸式的——UI 内容延伸到状态栏下面视觉上更整洁、更有代入感。但实现沉浸式有一个最大的坑内容可能被状态栏或导航条遮挡。这篇文章讲清楚加油站项目是怎么实现沉浸式的以及怎么优雅地处理安全区域问题。项目预览一、什么是沉浸式布局默认情况下HarmonyOS 应用的内容区域是这样的┌──────────────────────┐ │ 状态栏系统 │ ← 高度约 40-50px不属于应用 ├──────────────────────┤ │ │ │ 应用内容区域 │ ← 应用可用区域 │ │ ├──────────────────────┤ │ 导航栏系统 │ ← 高度约 34px有导航条的机型 └──────────────────────┘开启沉浸式后┌──────────────────────┐ │ 状态栏系统 │ ← 状态栏变透明内容延伸到这里 │ ─────────────────── │ │ │ │ 应用内容区域 │ ← 应用内容占满整个屏幕 │ 占满全屏 │ │ │ │ ─────────────────── │ │ 导航栏系统 │ ← 导航条变透明内容延伸到这里 └──────────────────────┘好看是好看了但有个问题如果不做处理你的文字/按钮可能会被状态栏或导航条遮住。二、开启沉浸式的代码// EntryAbility.ets → onWindowStageCreate()letisLayoutFullScreentrue;letwindowClass:window.WindowwindowStage.getMainWindowSync();windowClass.setWindowLayoutFullScreen(isLayoutFullScreen).then((){hilog.info(0x0000,testTag,Succeeded in setting the window layout to full-screen mode.);}).catch((err:BusinessError){hilog.error(0x0000,testTag,Failed to set the window layout to full-screen mode. Cause:JSON.stringify(err));});setWindowLayoutFullScreen(true)这一行就搞定了沉浸式布局之后应用内容会铺满整个屏幕包括状态栏和导航栏区域。三、获取安全区域高度沉浸式开启后下一步是获取状态栏和导航条的实际高度这样我们才能给内容设置合适的 padding避免遮挡// 获取底部导航条高度导航指示器区域lettypewindow.AvoidAreaType.TYPE_NAVIGATION_INDICATOR;letavoidAreawindowClass.getWindowAvoidArea(type);letbottomRectHeightavoidArea.bottomRect.height;AppStorage.setOrCreate(bottomRectHeight,bottomRectHeight);// 获取顶部状态栏高度系统状态栏区域typewindow.AvoidAreaType.TYPE_SYSTEM;avoidAreawindowClass.getWindowAvoidArea(type);lettopRectHeightavoidArea.topRect.height;AppStorage.setOrCreate(topRectHeight,topRectHeight);3.1 AvoidAreaType 有哪些类型类型描述典型场景TYPE_SYSTEM系统状态栏顶部显示时间、电量等TYPE_NAVIGATION_INDICATOR底部导航指示器Home键条iPhone 底部小横条、HarmonyOS 导航条TYPE_CUTOUT刘海/挖孔区域异形屏手机TYPE_KEYBOARD软键盘输入法弹出时3.2 avoidArea 的结构interfaceAvoidArea{topRect:Rect;// 上方避让矩形leftRect:Rect;// 左方避让矩形rightRect:Rect;// 右方避让矩形bottomRect:Rect;// 下方避让矩形}interfaceRect{left:number;// 矩形左边x坐标top:number;// 矩形顶部y坐标width:number;// 矩形宽度height:number;// 矩形高度我们需要的就是这个}所以状态栏高度 avoidArea.topRect.heightTYPE_SYSTEM导航条高度 avoidArea.bottomRect.heightTYPE_NAVIGATION_INDICATOR四、动态监听安全区域变化仅在onWindowStageCreate里读一次高度是不够的——手机竖横屏切换、折叠屏展开/折叠时高度都会变化// 注册监听器windowClass.on(avoidAreaChange,(data){if(data.typewindow.AvoidAreaType.TYPE_SYSTEM){lettopRectHeightdata.area.topRect.height;AppStorage.setOrCreate(topRectHeight,topRectHeight);}elseif(data.typewindow.AvoidAreaType.TYPE_NAVIGATION_INDICATOR){letbottomRectHeightdata.area.bottomRect.height;AppStorage.setOrCreate(bottomRectHeight,bottomRectHeight);}});当安全区域变化时这个回调会自动触发更新AppStorage里的值。由于页面用StorageProp绑定了这些值UI 会自动重新渲染安全区域适配是实时的。五、在页面中消费安全区域高度EntryAbility把高度存入AppStorage页面用StorageProp取出来// GasStationPage.etsComponentstruct GasStationPage{StorageProp(bottomRectHeight)bottomRectHeight:number0;// 底部导航条高度pxStorageProp(topRectHeight)topRectHeight:number0;// 顶部状态栏高度px// ...}5.1 换算单位px → vpgetWindowAvoidArea返回的高度单位是px物理像素但布局属性通常用vp虚拟像素。不过在这个项目里这个值直接用在了padding中HarmonyOS 的部分属性接受 px 值这里需要注意实际使用时可能需要换算// 如果需要换算import{display}fromkit.ArkUI;letdensitydisplay.getDefaultDisplaySync().densityDPI/160;// 获取屏幕密度lettopRectHeightVptopRectHeight/density;实际项目中由于topRectHeight和bottomRectHeight主要用于在特定场景下作为组件的 padding 偏移这种简单用法一般能正常工作。六、地图页的实际效果GasStationPage.ets使用了NavDestinationStack的结构build(){NavDestination(){Stack(){// 底层全屏地图自动铺满整个屏幕包括状态栏区域MapComponent({mapOptions:this.mapOptions,mapCallback:this.callback,});// 上层标题栏固定在顶部this.titleBuilder();}.width(100%).height(100%)// 底部弹窗配置....bindSheet(...)}.hideToolBar(true)// 隐藏系统工具栏.hideTitleBar(true)// 隐藏系统标题栏.height(100%).width(100%)}hideTitleBar(true)和hideToolBar(true)配合沉浸式把系统自带的标题栏和工具栏都隐藏让地图真正铺满全屏。标题栏的位置处理BuildertitleBuilder(){Row({space:Constants.SPACE_8}){Image($r(app.media.back)).width(40).height(40).onClick((){this.pageInfos.pop();});Text($r(app.string.car_life)).fontWeight(700).fontSize(20);}.width(100%).padding({left:16}).position({top:Constants.POSITION_TOP// 固定在距顶部 50px 的位置});}标题栏用.position({ top: 50 })固定在距顶部 50px 的地方避开了状态栏。提示这里直接用了固定的 50px更精确的做法是用topRectHeight动态计算.position({ top: this.topRectHeight 8 })这样在不同机型上都能正确避开状态栏。七、主页的状态栏适配主页MainPage使用Navigation组件它有内置的标题栏标题栏会自动处理状态栏的避让build(){Navigation(this.pageInfos){this.pageBuilder();}.title($r(app.string.car_life))// Navigation 的标题.width(100%).height(100%).backgroundColor($r(app.color.page_background));}Navigation的标题栏会自动出现在状态栏下方不需要手动计算高度。这是Navigation组件的一大优点——系统帮你处理好了安全区域。八、不同机型的差异机型状态栏高度导航条高度有实体Home键的旧机型~72px0无虚拟导航条全面屏有导航条~100px~68px折叠屏展开~40px~34px折叠屏折叠~100px~68px正因为差异大才需要动态获取而不是写死。总结沉浸式布局分三步走开启全屏setWindowLayoutFullScreen(true)读取高度getWindowAvoidArea()获取状态栏和导航条高度存入AppStorage动态更新on(avoidAreaChange)监听变化实时更新页面用StorageProp取出高度在需要避让的地方设置对应的 padding。搞定这三步沉浸式体验就稳了。下一篇讲AppStorage 全局状态共享——为什么高度要存在AppStorage里AppStorage到底是什么东西