HarmonyOS鸿蒙Next中如何在全屏沉浸式且在横屏的状态下获取状态栏,刘海屏,导航栏他们的间距

HarmonyOS鸿蒙Next中如何在全屏沉浸式且在横屏的状态下获取状态栏,刘海屏,导航栏他们的间距 我的app是横屏沉浸式的,但是里面的按钮图标等UI设计我不想被摄像头或者切图的挖空区遮挡,所以我需要在这种状态下获取合适的间距进行适当调节,或者说你有什么更好的办法能解决这个问题。因为在横屏状态下,背景图是全屏的但是里面的UI按钮并不想要被摄像头遮挡所以有没有更好的办法。我还是用flutter做的如果需要原生那边进行修改还得通信什么的

4 回复

【背景知识】

  1. 窗口全屏布局方案 调整布局系统为全屏布局,界面元素延伸到状态栏和导航条区域实现沉浸式效果。当不隐藏避让区时,可通过接口查询状态栏和导航条区域进行可交互元素避让处理,并设置状态栏或导航条的颜色等属性与界面元素匹配。当隐藏避让区时,通过对应接口设置全屏布局即可。

参考链接:窗口全屏布局方案

  1. 组件安全区方案 布局系统保持安全区内布局,然后通过接口延伸绘制内容(如背景色,背景图)到状态栏和导航条区域实现沉浸式效果。该方案下,界面元素仅做绘制延伸,无法单独布局到状态栏和导航条区域,针对需要单独布局UI元素到状态栏和导航条区域的场景建议使用窗口全屏布局方案处理。

参考链接:组件安全区方案

  1. padding:组件内边距 参考链接:组件尺寸设置

  2. @StorageLink 参考链接:应用全局的UI状态

  3. 摄像头挖孔区域 默认摄像头挖孔区域不为非安全区域,页面不避让挖孔。

参考链接:安全区域

【解决方案】

针对不同场景下的沉浸式效果需求,可以选择设置或不设置FullScreen属性,采用不同的解决方案处理对应的问题。

  1. 不设置FullScreen 当应用中大多数页面需要避让系统顶部状态栏和底部导航条时,可以不使用FullScreen模式,使用组件的扩展安全区域方式。当组件布局不需要避让时,可以通过expandSafeArea属性让组件渲染到非安全区。List组件还可以通过contentStartOffset和contentEndOffset设置内容顶部和底部的空余高度。

这种方案特别需要注意的是,默认摄像头挖孔区域不为非安全区域,页面不避让挖孔。在不设置FullScreen时,竖屏状态下摄像头挖孔区域会因为导航栏非安全区域的存在避让,但在横屏状态下不会主动避让。如果要实现横屏状态下页面默认避让摄像头挖空区域,需要在module.json5中添加配置项,使摄像头挖孔区域视为非安全区。

配置项如下:

"metadata": [
    {
        "name": "avoid_cutout",
        "value": "true",
    }
]
  1. 设置FullScreen 当应用中大多数页面需要全屏渲染时,可以在onWindowStageCreate方法中全局设置windowLayoutFullScreen为true。或者在需要的场景内单独设置windowLayoutFullScreen为true,退出时恢复原值。

在FullScreen模式下,Navigation的TitleBar会被顶部状态栏遮挡,底部Tab组件会被系统导航条遮挡。可以通过设置padding来手动避让,设置padding后子页面也会受到影响,需要更新Navigation根页面中设置的padding才生效,使用AppStorage可以设置此padding值。

更多关于HarmonyOS鸿蒙Next中如何在全屏沉浸式且在横屏的状态下获取状态栏,刘海屏,导航栏他们的间距的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


/*

  • Copyright © 2023 Hunan OpenValley Digital Industry Development Co., Ltd.
  • Licensed under the Apache License, Version 2.0 (the “License”);
  • you may not use this file except in compliance with the License.
  • You may obtain a copy of the License at
  • http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software
  • distributed under the License is distributed on an “AS IS” BASIS,
  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  • See the License for the specific language governing permissions and
  • limitations under the License. */

import { FlutterAbility, FlutterEngine } from ‘@ohos/flutter_ohos’; import { GeneratedPluginRegistrant } from ‘…/plugins/GeneratedPluginRegistrant’; import { CustomPlugin } from ‘./CustomPlugin’; import { NBWindow } from ‘./NBWindow’; import { window } from ‘@kit.ArkUI’; import HuaweiPaymentPlugin from ‘…/plugins/HuaweiPaymentPlugin’; import WindowManagerPlugin from ‘…/plugins/WindowManagerPlugin’;

export default class EntryAbility extends FlutterAbility { configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) this.addPlugin(new CustomPlugin()) this.addPlugin(new HuaweiPaymentPlugin()) // 支付插件 this.addPlugin(new WindowManagerPlugin()); //安全区域 }

onWindowStageCreate(windowStage: window.WindowStage): void {

// super.onWindowStageCreate(windowStage)
// NBWindow.setWindow(windowStage)

windowStage.loadContent('pages/AreaZone');

} }

import { window } from ‘@kit.ArkUI’; import { BusinessError } from ‘@kit.BasicServicesKit’;

export class NBWindow { private static windowStage: window.WindowStage; public static PORTRAIT: window.Orientation = window.Orientation.PORTRAIT; public static LANDSCAPE: window.Orientation = window.Orientation.LANDSCAPE;

// 其他方法 public static setWindow(_windowStage: window.WindowStage) { NBWindow.windowStage = _windowStage }

public static getWindow(): window.WindowStage { return NBWindow.windowStage }

// 新增:获取顶部状态栏高度(安全区顶部) public static async getStatusBarHeight(): Promise<number> { if (!NBWindow.windowStage) { throw new Error(“windowStage is not initialized”); } try { // 获取主窗口 const mainWindow = await NBWindow.windowStage.getMainWindowSync(); // 获取系统安全区域(包含状态栏) const systemArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); // 转换为VP单位并返回顶部高度 return px2vp(systemArea.topRect.height); } catch (err) { console.error(获取状态栏高度失败: ${(err as BusinessError).message}); return 0; } }

// 新增:获取底部导航栏高度(安全区底部) public static async getNavigationBarHeight(): Promise<number> { if (!NBWindow.windowStage) { throw new Error(“windowStage is not initialized”); } try { const mainWindow = await NBWindow.windowStage.getMainWindowSync(); // 获取导航栏安全区域 const navigationArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); // 转换为VP单位并返回底部高度 return px2vp(navigationArea.bottomRect.height); } catch (err) { console.error(获取导航栏高度失败: ${(err as BusinessError).message}); return 0; } }

public static setOrientation(ori: window.Orientation): void { let windowClass: window.Window | undefined = undefined; NBWindow.windowStage.getMainWindow((err: BusinessError, data) => { const errCode: number = err.code; if (errCode) { console.error(Failed to obtain the main window. Cause code: ${err.code}, message: ${err.message}); return; } windowClass = data; let orientation = ori; try { let promise = windowClass.setPreferredOrientation(orientation); promise.then(() => { console.info(‘Succeeded in setting the window orientation.’); }).catch((err: BusinessError) => { console.error(Failed to set the window orientation. Cause code: ${err.code}, message: ${err.message}); }); } catch (exception) { console.error(Failed to set window orientation. Cause code: ${exception.code}, message: ${exception.message}); } }); } }

@Entry @Component struct AreaZone { // 声明安全区高度变量(不依赖AppStorage) @State topHeight: number = 0; @State bottomHeight: number = 0;

// 组件初始化时获取安全区高度 async aboutToAppear() { try { // 从NBWindow获取顶部状态栏高度 this.topHeight = await NBWindow.getStatusBarHeight(); // 从NBWindow获取底部导航栏高度 this.bottomHeight = await NBWindow.getNavigationBarHeight(); } catch (err) { console.error(获取安全区高度失败: ${err}); } }

// 定义顶部安全区域样式 @Styles topSafeAreaStyle() { .padding({ top: this.topHeight }) }

// 定义底部安全区域样式 @Styles bottomSafeAreaStyle() { .padding({ bottom: this.bottomHeight }) }

build() { Column() { // 顶部内容(适配状态栏高度) Text(‘顶部内容’) .fontSize(18) .width(‘100%’) .height(this.topHeight) .backgroundColor(’#ff0000’) .topSafeAreaStyle()

  // 中间内容区域
  Column() {

    Text(`刘海屏/状态栏高度: ${this.topHeight}`)
      .fontSize(16)
      .padding(5)
      .backgroundColor('#e0e0e0')
      .borderRadius(4)
      .margin(5)

    Text(`底部导航栏高度: ${this.bottomHeight}`)
      .fontSize(16)
      .padding(5)
      .backgroundColor('#e0e0e0')
      .borderRadius(4)
      .margin(5)


  }
  .flexGrow(1)
  .width('100%')
  .justifyContent(FlexAlign.Center)
  .padding(10)

  // 底部内容(适配导航栏高度)
  Text('底部内容')
    .fontSize(18)
    .width('100%')
    .height(this.bottomHeight)
    .backgroundColor('#00ff00')
    .bottomSafeAreaStyle()
}
.width('100%')
.height('100%')

} }

import { window } from ‘@kit.ArkUI’; import { BusinessError } from ‘@kit.BasicServicesKit’;

@Entry @Component struct AreaZone { // 声明安全区高度变量 @State topHeight: number = 0; @State bottomHeight: number = 0;

// 组件初始化时获取安全区高度 async aboutToAppear() { try { // 直接通过window模块获取当前窗口实例 const windowClass = await window.getLastWindow(getContext(this));

  //  获取状态栏/刘海屏高度(安全区顶部)
  const avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  // 获取导航栏安全区域
  const navigationArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
  
  // 转换为VP单位(px转vp)
  this.topHeight = px2vp(avoidArea.topRect.height);
  this.bottomHeight = px2vp(navigationArea.bottomRect.height);

} catch (err) {
  console.error(`获取安全区高度失败: ${(err as BusinessError).message}`);
}

}

// 定义顶部安全区域样式 @Styles topSafeAreaStyle() { .padding({ top: this.topHeight }) }

// 定义底部安全区域样式 @Styles bottomSafeAreaStyle() { .padding({ bottom: this.bottomHeight }) }

build() { Column() { // 顶部内容(适配状态栏高度) Text(‘顶部内容’) .fontSize(18) .width(‘100%’) .height(this.topHeight) .backgroundColor(’#ff0000’) .topSafeAreaStyle()

  // 中间内容区域
  Column() {
    Text(`刘海屏/状态栏高度: ${this.topHeight}`)
      .fontSize(16)
      .padding(5)
      .backgroundColor('#e0e0e0')
      .borderRadius(4)
      .margin(5)

    Text(`底部导航栏高度: ${this.bottomHeight}`)
      .fontSize(16)
      .padding(5)
      .backgroundColor('#e0e0e0')
      .borderRadius(4)
      .margin(5)
  }
  .flexGrow(1)
  .width('100%')
  .justifyContent(FlexAlign.Center)
  .padding(10)

  // 底部内容(适配导航栏高度)
  Text('底部内容')
    .fontSize(18)
    .width('100%')
    .height(this.bottomHeight)
    .backgroundColor('#00ff00')
    .bottomSafeAreaStyle()
}
.width('100%')
.height('100%')

} }

import { BusinessError } from ‘@kit.BasicServicesKit’; import { common } from ‘@kit.AbilityKit’; import { FlutterPlugin, FlutterPluginBinding, MethodCall, MethodChannel, MethodResult, } from ‘@ohos/flutter_ohos’; import { window } from ‘@kit.ArkUI’;

const TAG = “WindowManagerPlugin”;

export default class WindowManagerPlugin implements FlutterPlugin { private channel?: MethodChannel; // 与Flutter通信的通道 private context: common.UIAbilityContext | null = null;

onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel = new MethodChannel(binding.getBinaryMessenger(), “samples.flutter.dev/window_manager”); // 从绑定中获取应用上下文(关键修正) // this.context = binding.getApplicationContext() as common.UIAbilityContext; this.context = getContext() as common.UIAbilityContext; this.channel.setMethodCallHandler({ onMethodCall: (call: MethodCall, result: MethodResult) => { if (!this.context) { result.error(TAG, ‘上下文未初始化’, null); return; } switch (call.method) { case “getSafeAreaInsets”: this.getSafeAreaInsets(result); break; default: result.notImplemented(); break; } } });

}

// 使用windowClass获取安全区域信息 private async getSafeAreaInsets(result: MethodResult) { try { // 直接通过window模块获取当前窗口实例 const windowClass = await window.getLastWindow(getContext(this)); // const windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口

  //  获取状态栏/刘海屏高度(安全区顶部)
  const avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  // 获取导航栏安全区域
  const navigationArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);

  // 转换为VP单位(px转vp)
  const topHeight = px2vp(avoidArea.topRect.height);
  const bottomHeight = px2vp(navigationArea.bottomRect.height);

  // 返回安全区域信息
  result.success({
    top: topHeight,
    bottom: bottomHeight,
    left: 20,
    right: 20

  });
} catch (error) {
  console.warn(TAG, `无法获得安全区域插图: ${(error as BusinessError).message}`);
  result.error(
    "获取插图失败",
    `无法获得安全区域插图: ${(error as BusinessError).message}`,
    error
  );
}

}

onDetachedFromEngine(binding: FlutterPluginBinding): void { console.warn(TAG, “onDetachedFromEngine”); this.channel?.setMethodCallHandler(null);

}

getUniqueClassName(): string { return “WindowManagerPlugin”; } }

在HarmonyOS鸿蒙Next中,获取全屏沉浸式横屏状态下的安全区域间距,需使用window模块。通过getWindowAvoidArea接口可获取避开区域数据:

import window from '@ohos.window';

let windowClass;
window.getLastWindow(this.context).then((win) => {
  windowClass = win;
  let avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  // avoidArea包含top/right/bottom/left四个边的避开像素值
});

该接口返回的AvoidArea对象包含状态栏、刘海屏和导航栏占据的像素值。横屏时需注意left/right属性可能对应物理顶部/底部区域。

在HarmonyOS Next中处理横屏沉浸式界面的安全区域问题,可以通过以下方式实现:

  1. 获取系统安全区域: 使用WindowInsets类获取系统栏位信息:
WindowInsets windowInsets = getWindow().getDecorView().getRootWindowInsets();
Insets statusBarInsets = windowInsets.getInsets(Type.statusBars());
Insets navigationBarInsets = windowInsets.getInsets(Type.navigationBars());
Insets displayCutoutInsets = windowInsets.getInsets(Type.displayCutout());
  1. Flutter端处理: 在Flutter中可以通过MediaQuery获取安全区域:
final padding = MediaQuery.of(context).padding;
  1. 横屏适配建议:
  • 将关键UI元素布局在安全区域内
  • 使用SafeArea组件自动避开系统栏位
  • 对于挖孔屏区域,可以通过DisplayCutout获取具体位置信息
  1. 原生与Flutter通信: 如果需要原生能力,可以通过MethodChannel实现通信:
// Flutter端调用
final result = await platform.invokeMethod('getWindowInsets');

注意:沉浸式模式下获取的insets值可能会变化,建议在onApplyWindowInsets回调中实时更新布局。

回到顶部