一、概述

在移动应用开发中,不同设备的屏幕形态各异,如刘海屏、全面屏、折叠屏等,同时系统状态栏、导航栏、软键盘等元素也会占据屏幕空间。为了确保应用内容在各种设备和场景下都能正常显示,不被遮挡,RN、Flutter和H5提供了一系列区域避让的机制和接口。此外,随着折叠屏设备逐渐普及,为了让应用在折叠屏设备上提供良好的用户体验,需要对布局进行特殊处理,RN和Flutter为此推出了折叠屏布局专用组件:FolderStack和FoldSplitContainer。本文将详细介绍各个框架区域避让和折叠屏布局组件的实现原理、适配指导以及具体的场景案例。

使用场景

典型应用全屏窗口UI元素包括状态栏、应用界面和底部导航条,其中状态栏和导航条,通常在全屏沉浸式布局下称为避让区,避让区之外的区域称为安全区。界面元素在不同设备上存在差异,下面是不同设备上状态栏、挖孔区、导航栏的位置差异,包括:直板机、PAD、PC、折叠屏(小折叠、双折、三折)。

说明

下图中标记区域含义:1为状态栏、2为挖孔区、3为导航栏。

图5-1 直板机界面元素示意图

 

图5-2 PAD界面元素示意图

    图5-3 PC界面元素示意图

    图5-4 折叠屏--小折叠 界面元素示意图(左图展开态 右图折叠态)

    图5-5 折叠屏--双折叠 界面元素示意图(左图折叠态 右图展开态)

      图5-6 折叠屏--三折叠 界面元素示意图(左图折叠态 右图二屏折叠态 下图三屏全展开态)

      面对上述不同设备的避让区差异,可以按照如下三种场景实现应用沉浸式效果:

      1.1 安全区布局场景

      布局系统保持安全区内布局,确保应用内容不会延伸到状态栏、导航栏区域。

      1.2 安全区布局+背景沉浸模式

      布局系统保持安全区内布局,然后延伸绘制内容(如背景色、背景图)到状态栏和导航条区域,实现沉浸式效果。

      1.3 全屏布局+避让场景

      布局系统保持全屏布局,通过相关接口获取避让区域位置、大小等信息,调整元素位置,确保不会被避让区遮挡。

      二、实现原理

      2.1 RN区域避让实现原理

      区域避让主要借助AvoidArea接口提供的能力实现,该接口由@hadss/react_native_avoid_area库提供。该接口包含几个核心方法,用于处理与区域避让相关的操作。在不同平台上,这些方法的实现和使用存在差异。

      • getWindowAvoidArea:此方法接收一个AvoidAreaType避让区域类型的参数,通过该参数可以获取当前应用窗口内容需要规避的区域。这些区域可能包括系统栏、刘海屏、手势操作区、软键盘等与窗口内容重叠时需要避让的区域。接口返回信息中包含对应避让区域是否可见、位置信息及宽高信息,应用可以通过这些信息调整内容布局,避免内容被遮挡。

      表5-1 AvoidAreaType避让类型枚举

      名称

      说明

      TYPE_SYSTEM

      0

      表示系统默认区域。通常表示状态栏区域,悬浮窗状态下的应用主窗中表示三点控制栏区域。

      TYPE_CUTOUT

      1

      表示刘海屏区域。

      TYPE_SYSTEM_GESTURE

      2

      表示手势区域。当前,各设备均无此类型避让区域。

      TYPE_KEYBOARD

      3

      表示软键盘区域。

      TYPE_NAVIGATION_INDICATO

      4

      表示底部导航条区域。

      接口返回信息结构如下:

      {
          "visible": true, // 是否遮挡布局
          "leftRect": { // 左侧避让区域
          "left": 0, // 距离左侧的边距
              "top": 0, // 距离上方的边距
              "width": 0, // 避让区域宽度
          "height": 0 // 避让区域高度
          },
          "topRect": { // 上方避让区域
            // 结构与leftRect相同
          },
          "rightRect": { // 右侧避让区域
            // 结构与leftRect相同
          },
          "bottomRect": { // 下方避让区域
            // 结构与leftRect相同
          }
      }

      接口使用示例如下:

      // AvoidArea避让区域信息,AvoidAreaType避让区域类型
      import { AvoidArea, AvoidAreaType } from "@hadss/react_native_avoid_area/src/turbo/NativeAvoidModule";
      // AvoidArea API
      import { Avoid } from '@hadss/react_native_avoid_area/src/index';
      // 获取避让区域
      let avoidArea = Avoid.getWindowAvoidArea(type);
      • addAvoidAreaListener:该方法用于添加系统规避区变化事件的监听。当系统规避区发生变化时,如软键盘弹出或收起、设备旋转等,会触发相应的回调函数,应用可以在回调中更新布局。使用示例如下:
      // AvoidArea避让区域信息,AvoidAreaType避让区域类型
      import { AvoidArea, AvoidAreaType } from "@hadss/react_native_avoid_area/src/turbo/NativeAvoidModule";
      // AvoidArea API
      import { Avoid } from '@hadss/react_native_avoid_area/src/index';
      // 添加系统规避区变化事件的监听
      Avoid.addAvoidAreaListener(data => {
        // 开发者基于AvoidAreaType自行逻辑处理
      });
      • removeAvoidAreaListener:用于移除之前添加的系统规避区变化事件监听,避免不必要的回调触发,节省系统资源。使用示例如下:
      // AvoidArea API
      import { Avoid } from '@hadss/react_native_avoid_area/src/index';
      // 移除系统规避区变化事件的监听
      Avoid.removeAvoidAreaListener();

      2.2 Flutter区域避让实现原理

      区域避让主要借助 AvoidAreaApi 这个对外提供能力的接口来实现。该接口包含几个核心方法,用于处理与区域避让相关的操作。

      • getWindowAvoidArea: 此方法接收一个 AvoidAreaType 类型的参数,通过该参数可以获取当前应用窗口内容需要规避的区域。这些区域可能包括系统栏、刘海屏、手势操作区、软键盘等与窗口内容重叠时需要避让的区域。例如,在处理刘海屏设备时,应用可以通过该方法获取刘海屏区域的信息,从而调整内容布局,避免内容被刘海遮挡。

      AvoidAreaPlugin.ets 类中的 getWindowAvoidArea 方法负责获取避让区域信息。通过 FlutterManager 获取窗口实例,然后调用窗口的 getWindowAvoidArea 方法获取相应类型的避让区域,并将结果通过 MethodResult 返回给 Flutter 端。

      // packages/avoid_area/ohos/src/main/ets/hadss/avoid_area/AvoidAreaPlugin.ets
      getWindowAvoidArea(type: window.AvoidAreaType, result: MethodResult) {
        const windowAvoidArea = this.windowClass?.getWindowAvoidArea(type ?? window.AvoidAreaType.TYPE_SYSTEM);
        result.success(JSON.stringify(windowAvoidArea));
      }
      • addAvoidAreaListener:该方法用于添加系统规避区变化事件的监听。当系统规避区发生变化时,如软键盘弹出或收起、设备旋转等,会触发相应的回调函数,应用可以在回调中更新布局。

      AvoidAreaPlugin.ets 类中的 addAvoidAreaListener 方法通过监听窗口的 avoidAreaChange 事件,当避让区域发生变化时,将变化信息通过 EventSink 发送给 Flutter 端。

      // packages/avoid_area/ohos/src/main/ets/hadss/avoid_area/AvoidAreaPlugin.ets
      addAvoidAreaListener(eventSink?: EventSink) {
        this.windowClass?.on('avoidAreaChange', (avoidAreaOptions: window.AvoidAreaOptions) => {
          eventSink?.success(JSON.stringify(avoidAreaOptions))
        })
      }
      • removeAvoidAreaListener:用于移除之前添加的系统规避区变化事件监听,避免不必要的回调触发,节省系统资源。

      AvoidAreaPlugin.ets 类中的 removeAvoidAreaListener 方法通过调用窗口的 off 方法移除 avoidAreaChange 事件的监听。

      // packages/avoid_area/ohos/src/main/ets/hadss/avoid_area/AvoidAreaPlugin.ets
      removeAvoidAreaListener() {
        this.windowClass?.off('avoidAreaChange');
      }

      2.3 Flutter折叠屏布局组件实现原理

      FoldSplitContainer

      folder_split_view.dart 定义了 FoldSplitContainer 组件,用于实现折叠屏二分栏、三分栏在展开态、悬停态以及折叠态的区域控制。

      • 状态监听:通过 FolderStackPlugin.folderStateEvents 监听折叠屏状态变化,当状态变化时更新界面。
      • 布局构建:根据不同的折叠屏状态(展开、半折叠、折叠)调用不同的布局构建方法,如 _buildExpandedLayout、_buildHoverModeLayout 和 _buildFoldedRegionLayout。
      • 比例转换:通过 _ratioToFlex 方法将比例转换为整数 flex 因子,用于 Flex 组件的布局。

      FolderStack

      folder_stack.dart 定义了 FolderStack 组件,继承于 Stack 控件,新增了折叠屏悬停能力,通过识别 upperItems 自动避让折叠屏折痕区后移到上半屏。

      • 状态监听:同样通过 FolderStackPlugin.folderStateEvents 监听折叠屏状态变化。
      • 布局处理:当设备处于半折叠且折痕方向为水平时,将 upperItems 中的子组件堆叠到上半屏,其他组件堆叠在下半屏。
      • 上下屏:通过折痕区域范围算出上下屏占用比例,然后使用 Flex 组件的布局。
      • FolderStateStreamHandler.ets 文件实现了折叠屏状态的监听功能,当折叠屏状态变化时,将状态信息通过 EventSink 发送给 Flutter 端。
      // packages/avoid_area/ohos/src/main/ets/hadss/avoid_area/FolderStateStreamHandler.ets
      export class FolderStateStreamHandler implements StreamHandler {
        private windowEventSink?: EventSink;
        private preSuccEvent: string = '';
      
        constructor() {
          if (display.isFoldable()) {
            display.on('foldStatusChange', this.foldStatusChangeCallback)
            display.on('change', this.changeCallback)
          }
        }
      }

      接口桥接和公用封装主要在 folder_options.dart 和 folder_plugin.dart 中实现。

      • folder_options.dart 定义了折叠屏布局的各种配置选项,如 FoldedRegionLayoutOptions、ExpandedRegionLayoutOptions 等,以及 FoldingFeature 类用于解析从原生传递过来的折叠屏状态信息。
      /// packages/avoid_area/lib/component/folder_options.dart
      /// 从原生传递过来的参数,折叠屏状态
      /// JSON传递 {"state":1,"orientation":0,"bounds":{"left":0,"right":1840,"top":1104,"bottom":1104}}
      /// state(折叠状态):0:未知,1:展开,2:折叠,3:半折叠
      /// orientation(折痕方向):0:水平,1:垂直
      /// bounds(折叠区域):包含四个属性,left、right、top、bottom,分别表示折叠区域的左、右、上、下边界,单位为像素。
      class FoldingFeature {
        ///折叠状态
        final FoldStatus state;
      
        ///折痕方向
        final Axis orientation;
      
        ///折叠区域
        final Rect bounds;
      
        FoldingFeature(
            {this.state = FoldStatus.unknown,
            this.orientation = Axis.horizontal,
            this.bounds = Rect.zero});
      
        factory FoldingFeature.fromJson(Map<String, dynamic> json) {
          FoldStatus stateFromInt(int state) {
            switch (state) {
              case 1:
                return FoldStatus.expanded;
              case 2:
                return FoldStatus.folded;
              case 3:
                return FoldStatus.halfFolded;
              default:
                return FoldStatus.unknown;
            }
          }
      
          Axis axisFromInt(int orientation) {
            switch (orientation) {
              case 1:
                return Axis.vertical;
              default:
                return Axis.horizontal;
            }
          }
      
          return FoldingFeature(
            state: stateFromInt(json['state']),
            orientation: axisFromInt(json['orientation']),
            bounds: Rect.fromLTRB(
                (json['bounds']["left"]).toDouble(),
                (json['bounds']["top"]).toDouble(),
                (json['bounds']["right"]).toDouble(),
                (json['bounds']["bottom"]).toDouble()),
          );
        }
      }
      • folder_plugin.dart 定义了 FolderStackPlugin 类,用于与原生进行通信,通过 EventChannel 监听折叠屏状态变化。
      /// packages/avoid_area/lib/component/folder_plugin.dart
      class FolderStackPlugin {
        static const EventChannel _folderStatusEventChannel =
            EventChannel('avoid_area/folder_state');
        static Stream<FoldingFeature>? _folderStatusEvents;
      
        ///折叠屏信息变换监听
        static Stream<FoldingFeature> get folderStateEvents {
          try {
            _folderStatusEvents ??= _repeatLatest(
              _folderStatusEventChannel
                  .receiveBroadcastStream()
                  .map((event) => FoldingFeature.fromJson(jsonDecode(event))),
            );
            return _folderStatusEvents!;
          } catch (e) {
            return const Stream.empty();
          }
        }
      }

      2.3 H5区域避让实现原理

      区域避让主要通过原生接口提供的能力实现,该接口包含几个核心方法,用于处理与区域避让相关的操作。

      • avoidAreaListener:该方法用于添加系统规避区变化事件的监听。通过监听"avoidAreaChange"参数,当系统规避区发生变化时,如软键盘弹出或收起、设备旋转等,会触发相应的回调函数,在回调中通过 runJavaScript() 的方法将变化的数据传递给H5页面。
      async avoidAreaListener(): Promise<void> {
        const windowClass = await this.webClassProxy.getMainWindow();
        try {
          windowClass.on('avoidAreaChange', (data) => {
            this.webController.runJavaScript(`
              window.dispatchEvent(new CustomEvent('statusBarChange',{ detail: ${JSON.stringify(data)} }));
            `);
            Logger.debug('Succeeded in enabling the listener for system avoid area changes. type:' +
            JSON.stringify(data.type) + ', area: ' + JSON.stringify(data.area));
          });
        } catch (exception) {
          Logger.error(`Failed to enable the listener for system avoid area changes. Cause code: ${exception.code}, message: ${exception.message}`);
        }
      }
      • registerJavaScriptProxy注入JavaScript对象到window对象中,并在window对象中调用该对象的方法。实现H5页面调用原生方法。
      Web({ src: CommonConstants.H5_URL, controller: this.webController })
        .onControllerAttached(() => {
          this.webController.registerJavaScriptProxy(this.webClassProxy, 'webClass',
            ['getAvoidArea', 'pxToVp', 'isFoldAndTablet', 'getWindowStatusType'],
            [],
            `{"javascriptProxyPermission":{"urlPermissionList":[{"scheme":"http","host":"${CommonConstants.HOST}","port":"${CommonConstants.PORT}","path":""}]}}`);
          this.webController.refresh();
        })
      • getAvoidArea:Plugins 类中的 getAvoidArea() 接口负责获取系统默认避让区域信息。此方法接收一个 AvoidAreaType 类型的参数,通过该参数可以获取当前应用窗口内容需要规避的区域。这些区域可能包括系统栏、刘海屏、手势操作区、软键盘等与窗口内容重叠时需要避让的区域。通过原生接口获取窗口实例,然后调用窗口的 getWindowAvoidArea 方法获取相应类型的避让区域,接口返回信息中包含对应避让区域是否可见、位置信息及宽高信息,并将结果返回给 H5 端。
      async getAvoidArea(type: window.AvoidAreaType) {
        const windowClass = await this.getMainWindow();
        let avoidArea = windowClass.getWindowAvoidArea(type);
        return avoidArea;
      }

      三、适配指导

      3.1 安全区布局场景

      图5-7 AvoidArea实现安全布局

      • RN适配指导

      安全区布局场景主要是为了确保应用内容不会被系统状态栏、导航栏等遮挡。通过上述的 AvoidArea  API获取的避让区域相关信息,包括状态栏、导航栏以及挖孔区,给外层容器设置padding,防止内部组件被避让区遮挡,从而实现安全布局效果。示例代码如下:

      const FullScreenView = () => {
          const [topPadding, setTopPadding] = useState(0);
          const [bottomPadding, setBottomPadding] = useState(0);
          const pixelRatio = PixelRatio.get() ? PixelRatio.get() : 1;
          useEffect(() => {
              const getAvoidArea = () => {
                  // 获取状态栏高度,设置上padding
                  let type = AvoidAreaType.TYPE_SYSTEM;
                  let avoidArea = Avoid.getWindowAvoidArea(type);
                  let topHeight = avoidArea.topRect.height / pixelRatio;
                  setTopPadding(topHeight);
                  // 获取导航栏高度,设置下padding
                  type = AvoidAreaType.TYPE_NAVIGATION_INDICATOR;
                  avoidArea = Avoid.getWindowAvoidArea(type);
                  let bottomHeight = avoidArea.bottomRect.height / pixelRatio;
                  setBottomPadding(bottomHeight);
              };
              getAvoidArea();
              const listener = Avoid.addAvoidAreaListener((data) => {
              // 监听避让区域变化,逻辑处理
              });
              return () => {
                  Avoid.removeAvoidAreaListener();
              };
          }, []);
          return (
              <View style={[styles.container, { paddingTop: topPadding, paddingBottom: bottomPadding }]}>
                  <View style={styles.contentContainer}>
                      <Text style={[styles.textStyle, styles.otherTextStyle]}>title</Text>
                      <Text style={[styles.textStyle, styles.contentTextStyle]}>content</Text>
                      <Text style={[styles.textStyle, styles.otherTextStyle]}>footer</Text>
                  </View>
              </View>
          );
      };
      export default FullScreenView;

      RN也提供了 SafeAreaView 安全布局组件,该组件可以自动将内容放置在安全区域内。在HarmonyOS上,这种方式存在底部导航栏没有自动避让的问题,因此建议使用上述的 AvoidArea API方式实现安全区布局场景。

      • Flutter适配指导

      安全区布局场景主要是为了确保应用内容不会被系统状态栏、导航栏等遮挡。Flutter 提供了 SafeArea 组件,该组件可以自动将内容放置在安全区域内。同时,也可以结合 AvoidAreaApi 获取的避让区域信息进行更精确的布局。

      SafeArea(
          child: Container(
              // 这里可以放置应用的主要内容
              child: Text('这是安全区布局内的内容'),
          ),
      );
      • H5适配指导

      该场景有两种实现方法:

      1. webview在安全区布局,H5页面不需要做额外处理

      import { webview } from '@kit.ArkWeb';
      
      @Component
      export struct CustomWebView {
        webController: webview.WebviewController = new webview.WebviewController();
      
        build() {
          RelativeContainer() {
            Web({ src: CommonConstants.H5_URL, controller: this.webController })
          }
          .height('100%')
          .width('100%')
        }
      }

      2. webview全屏布局,H5获取状态栏、导航栏高度,设置padding

      (1)原生侧开启全屏布局

      在HarmonyOS工程的EntryAbility.ets文件中的onWindowStageCreate()生命周期内,使用setWindowLayoutFullScreen()实现界面元素延伸到状态栏和导航区域;

      示例代码如下:

      windowObj: window.Window | undefined = undefined;
      onWindowStageCreate(windowStage: window.WindowStage): void {
        windowStage.getMainWindow((err: BusinessError<void>, windowObj) => {
          this.windowObj = windowObj;
          // 设置全屏
          windowObj.setWindowLayoutFullScreen(true);
        })
      }

      (2)注入JavaScript对象

      使用registerJavaScriptProxy() 接口将原生侧方法注入到H5页面中,H5页面可以调用原生方法获取状态栏和导航区域高度。

      Web({ src: CommonConstants.H5_URL, controller: this.webController })
        .onControllerAttached(() => {
          this.webController.registerJavaScriptProxy(this.webClassProxy, 'webClass',
            ['getAvoidArea', 'pxToVp', 'isFoldAndTablet', 'getWindowStatusType'],
            [],
            `{"javascriptProxyPermission":{"urlPermissionList":[{"scheme":"http","host":"${CommonConstants.HOST}","port":"${CommonConstants.PORT}","path":""}]}}`);
          this.webController.refresh();
        })

      (3)H5页面示例代码

      页面通过调用原生方法获取状态栏和导航条高度,并设置在外层容器中,实现状态栏和导航条的避让。

      <template>
        <div class="contain" :style="{
            paddingTop: `${paddingTop}px`,
            paddingBottom: `${paddingBottom}px`
          }">
          <div class="header"></div>
          <div class="body"> </div>
          <div class="footer"></div>
        </div>
      </template>
      
      <script setup lang="ts">
      import { onMounted, onBeforeUnmount } from 'vue';
      
      const paddingTop = 0;
      const paddingBottom = 0;
      onMounted(() => {
        if ((window as any).webClass) {
          // 获取状态栏安全区高度
          const paddingTop = await (window as any).webClass.getAvoidArea(AvoidAreaType.TYPE_SYSTEM).area.topRect.height;
          // 获取导航条安全区高度
          const paddingBottom = await (window as any).webClass.getAvoidArea(AvoidAreaType.TYPE_NAVIGATION_INDICATOR).area.bottomRect.height;
        }
      });
      </script>

      3.2 安全区布局+背景沉浸模式

      图5-8 AvoidArea实现安全区布局+背景沉浸模式

      • RN适配指导

      在安全区域布局的基础上,延伸绘制内容(如背景色、背景图)到状态栏和导航条区域,实现沉浸式效果。上述示例代码中FullScreenView已经实现了安全区布局,我们可以在这个组件的基础上延伸绘制内容。在FullScreenView的外层套一层View容器,用于设置背景,这样就可以实现背景沉浸模式。示例代码如下:

       return (
          <View style={styles.backgroundContainer}>
            <FullScreenView></FullScreenView>
          </View>
        );
        const styles = StyleSheet.create({
          backgroundContainer: {
              height: '100%',
              width:'100%',
              backgroundColor: 'gray',
          }
        })
      • Flutter适配指导

      在背景沉浸模式下,需要使用 Stack 组件,背景全屏展示,然后使用 SafeArea 包裹安全区域 。

      Stack(
        children: [
          Image.asset(
            'assets/background.png',
            fit: BoxFit.cover,
            width: double.infinity,
            height: double.infinity,
          ),
          SafeArea(
            child: Container(
              child: Text('这是安全区布局内的内容'),
            ),
          ),
        ],
      )
      • H5适配指导

      在安全区域布局的基础上,延伸绘制内容(如背景色、背景图)到状态栏和导航条区域,实现沉浸式效果。实现方式与安全区布局相似,将webview设置全屏沉浸式,H5背景全屏、内容设置padding。

      1. 原生侧开启全屏布局

      在工程的EntryAbility.ets文件中的onWindowStageCreate()生命周期内,使用setWindowLayoutFullScreen()实现界面元素延伸到状态栏和导航区域。

      示例代码如下:

      windowObj: window.Window | undefined = undefined;
      onWindowStageCreate(windowStage: window.WindowStage): void {
        windowStage.getMainWindow((err: BusinessError<void>, windowObj) => {
          this.windowObj = windowObj;
          // 设置全屏
          windowObj.setWindowLayoutFullScreen(true);
        })
      }

      2. 注入JavaScript对象

      使用registerJavaScriptProxy() 接口将原生侧方法注入到H5页面中,H5页面可以调用原生方法获取状态栏和导航区域高度。

      Web({ src: CommonConstants.H5_URL, controller: this.webController })
            .onControllerAttached(() => {
              this.webController.registerJavaScriptProxy(this.webClassProxy, 'webClass',
                ['getAvoidArea', 'pxToVp', 'isFoldAndTablet', 'getWindowStatusType'],
                [],
                `{"javascriptProxyPermission":{"urlPermissionList":[{"scheme":"http","host":"${CommonConstants.HOST}","port":"${CommonConstants.PORT}","path":""}]}}`);
              this.webController.refresh();
            })

      3. H5页面示例代码

      页面通过调用原生方法获取状态栏和导航条高度,并设置在外层容器中,实现状态栏和导航条的避让。

      <template>
        <div class="contain" >
          <div class="header" :style="{
            paddingTop: `${paddingTop}px`
          }">
          </div>
          <div class="body"> </div>
          <div class="footer" :style="{
            paddingBottom: `${paddingBottom}px`
          }">
          </div>
        </div>
      </template>
      
      <script setup lang="ts">
      import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
      
      const paddingTop = 0;
      const paddingBottom = 0;
      onMounted(() => {
        if ((window as any).webClass) {
          // 获取状态栏安全区高度
          const paddingTop = await (window as any).webClass.getAvoidArea(AvoidAreaType.TYPE_SYSTEM).area.topRect.height;
          // 获取导航条安全区高度
          const paddingBottom = await (window as any).webClass.getAvoidArea(AvoidAreaType.TYPE_NAVIGATION_INDICATOR).area.bottomRect.height;
        }
      });
      </script>

      3.3 全屏布局+避让场景

      图5-9 AvoidArea实现全屏布局+避让场景

      • RN适配指导

      1. 原生侧开启全屏布局

      在HarmonyOS工程的EntryAbility.ets文件中的onWindowStageCreate()生命周期内:

      1、setWindowLayoutFullScreen()实现界面元素延伸到状态栏和导航区域;

      2、再通过setWindowSystemBarEnable()将主窗口状态栏和底部导航条隐藏,实现页面的全屏布局。

      示例代码如下:

      async onWindowStageCreate(windowStage: window.WindowStage) {
          windowStage.getMainWindow((err: BusinessError, data) => {
          let windowClass: window.Window | undefined = undefined;
          windowClass = data;
          // 开启全屏
          let orientation = window.Orientation.AUTO_ROTATION_RESTRICTED;
          windowClass.setPreferredOrientation(orientation);
          // 隐藏状态栏、导航条
          let names: Array<'status' | 'navigation'> = [];
          windowClass.setWindowSystemBarEnable(names)
        })
      }

      2. 避让刘海屏/挖孔区

      在RN工程中通过AvoidArea API的接口getWindowAvoidArea()获取挖孔区避让数据,计算并调整子元素的位置,规避挖孔区。示例代码如下:

      let type = AvoidAreaType.TYPE_CUTOUT;
      //获取刘海屏/挖孔区位置信息
      let avoidArea = Avoid.getWindowAvoidArea(type);
      //...获取避让区信息后计算并调整子元素的位置,规避挖孔区
      • Flutter适配指导

      在全屏布局中,需要隐藏系统状态栏和导航栏,并添加避让区域变化监听。根据避让区域的变化动态调整布局,确保应用在各种情况下都能正常显示。

      1. 隐藏状态栏和导航栏

      SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays:[]);

      2. 添加避让区域变化监听,根据避让区域的变化动态调整布局

      /// samples/avoid_area_sample/lib/game_page.dart
      AvoidAreaApi.addAvoidAreaListener((event) {
        var result = json.decode(event);
        AvoidArea avoidArea = AvoidArea.fromJson(result["area"]);
        switch(AvoidAreaType.values[result["type"]]){
          // 状态栏
          case AvoidAreaType.system:
            avoidAreaSystem = avoidArea;
            break;
          // 刘海区域
          case AvoidAreaType.cutout:
            avoidAreaCutout = avoidArea;
            break;
          // 键盘区域
          case AvoidAreaType.keyboard:
            avoidAreaKeyboard = avoidArea;
            break;
          // 手势区域
          case AvoidAreaType.systemGesture:
            avoidAreaSystemGesture = avoidArea;
            break;
          // 底部导航栏
          case AvoidAreaType.navigationIndicator:
            avoidAreaNavigationIndicator = avoidArea;
            break;
        }
        update();
      });
      • H5适配指导

      在全屏布局中,需要隐藏系统状态栏和导航栏,并添加避让区域变化监听。根据避让区域的变化动态调整布局,确保应用在各种情况下都能正常显示。

      1. 原生侧开启全屏布局

      在HarmonyOS工程的EntryAbility.ets文件中的onWindowStageCreate()生命周期内:

      1、setWindowLayoutFullScreen()接口实现界面元素延伸到状态栏和导航区域;

      2、再通过setWindowSystemBarEnable()接口将主窗口状态栏和底部导航条隐藏,实现页面的全屏布局。

      示例代码如下:

      windowObj: window.Window | undefined = undefined;
      onWindowStageCreate(windowStage: window.WindowStage): void {
        windowStage.getMainWindow((err: BusinessError<void>, windowObj) => {
          this.windowObj = windowObj;
          // 隐藏状态栏、导航条
          windowObj.setSpecificSystemBarEnabled('status', false);
          windowObj.setSpecificSystemBarEnabled('navigationIndicator', false);
          // 设置全屏
          windowObj.setWindowLayoutFullScreen(true);
        })
      }

      2. 注入JavaScript对象

      使用registerJavaScriptProxy() 接口将原生侧方法注入到H5页面中,H5页面可以调用原生方法获取状态栏和导航区域高度。

      Web({ src: CommonConstants.H5_URL, controller: this.webController })
        .onControllerAttached(() => {
          this.webController.registerJavaScriptProxy(this.webClassProxy, 'webClass',
            ['getAvoidArea', 'pxToVp', 'isFoldAndTablet', 'getWindowStatusType'],
            [],
            `{"javascriptProxyPermission":{"urlPermissionList":[{"scheme":"http","host":"${CommonConstants.HOST}","port":"${CommonConstants.PORT}","path":""}]}}`);
          this.webController.refresh();
        })

      3. H5页面中计算刘海屏/挖孔区位置

      页面通过调用原生方法获取刘海屏区域位置,通过计算转换成相对位置和避让的数值。

      static async getAvoidAreaLocation(): Promise<AreaAndMargin> {
        let location = AreaLocation.none;
        if ((window as any).webClass) {
          const avoidArea = await (window as any).webClass.getAvoidArea();
      
          //根据取避让区信息计算并调整子元素的位置,并返回AreaLocation和距离,下面是避让区域处于顶部的情况
          if (this.cameraIsHere(avoidArea.topRect)) {
            const left = AreaUtils.px2vp(avoidArea.topRect.left);
            const right = AreaUtils.px2vp(avoidArea.topRect.left + avoidArea.topRect.width);
            const center = window.screen.width / 2;
            let marginValue = left;
            if (left <= center && center <= right) {
              location = AreaLocation.topCenter;
            } else if (center < left) {
              location = AreaLocation.rightTop;
              marginValue = window.screen.width - AreaUtils.px2vp(avoidArea.topRect.left);
            } else {
              location = AreaLocation.leftTop;
            }
            return { areaLocation: location, marginValue: marginValue };
          }
          //底部以及左右两边的情况也相应做处理
          ......    
        }
        return { areaLocation: location, marginValue: 0 };
      }

      4. H5页面示例代码

      页面通过调用原生方法获取避让区域,并监听避让区域变化事件,根据回调设定不同区域的样式,实现避让。

      <template>
        <div class="contain">
          <div class="header" :style="{
            marginTop: `${topMargin}px`
          }">
          </div>
          <div class="body"> </div>
          <div class="footer" :style="{
            marginBottom: `${bottomMargin}px`
          }">
          </div>
        </div>
      </template>
      
      <script setup lang="ts">
      import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
      import { AreaUtils } from '../utils/AreaUtils';
      import { AvoidAreaType } from '../common/windowCommon';
      
      const topMargin = ref(AreaUtils.getInstance().getTopMargin());
      const bottomMargin = ref(AreaUtils.getInstance().getBottomMargin());
      const margins = reactive({
        leftTopMargin: 0,
        rightTopMargin: 0,
        leftBottomMargin: 0,
        rightBottomMargin: 0,
        leftCenterMargin: 0,
        rightCenterMargin: 0
      });
      
      const reCulcAvoidArea = () => {
        AreaUtils.getAvoidAreaLocation().then((res) => {
          AreaUtils.getInstance().setAvoidValue(res.areaLocation, res.marginValue);
          // 计算摄像头、刘海位置
          Object.assign(margins, AreaUtils.getAllMargin());
        });
      };
      
      // 监听回调事件
      const handleStatusBarChange = (event: any) => {
        const data = event.detail;
        const areaType = data.type;
        reCulcAvoidArea();
        // 根据不同AvoidAreaType设定不同区域的区域的Margin
        if (areaType === AvoidAreaType.TYPE_SYSTEM) {
          topMargin.value = AreaUtils.px2vp(data.area.topRect.height);
        }
        if (areaType === AvoidAreaType.TYPE_CUTOUT) {
          reCulcAvoidArea();
        }
        if (areaType === AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
          bottomMargin.value = AreaUtils.px2vp(data.area.bottomRect.height);
        }
      };
      
      onMounted(() => {
        reCulcAvoidArea();
        // 注册事件监听
        window.addEventListener("statusBarChange", handleStatusBarChange);
      });
      
      onBeforeUnmount(() => {
        // 注销事件监听
        window.removeEventListener("statusBarChange", handleStatusBarChange);
      });
      </script>

      3.4 折叠屏布局组件适配指导

      • RN适配指导

      FolderStack 和 FoldSplitContainer 这两个组件是折叠屏布局的专用组件。下面对这两个组件进行使用指导。

      1. FolderStack组件

      FolderStack继承于Stack(层叠布局)控件,新增了折叠屏悬停能力,通过识别upperItems自动避让折叠屏折痕区后移到上半屏。

      <FolderStack
           // 定义悬停态会被移到上半屏的子组件的id,组件id在此数组中的子组件悬停触发时自动避让折叠屏折痕区后移到上半屏,其它组件堆叠在下半屏区域。
           upperItems={["upperitemsId"]}
           // 是否使用默认动效。默认值:true,设置true表示使用默认动效,设置false表示不使用默认动效
           enableAnimation={true}
           // autoHalfFold 是否开启自动旋转。默认值:true,设置true表示开启自动旋转,设置false表示关闭自动旋转。
           autoHalfFold={true}
           // alignContent子组件在容器内的对齐方式。
           alignContent='End'
           // onFolderStateChange当折叠状态改变的时候回调,仅在横屏状态下生效。
           onFolderStateChange={(foldStatus) => {}}
           // onHoverStatusChange当悬停状态改变的时候回调。
           onHoverStatusChange={(HoverStatus) => {}}>
           {/* 子组件id与父组件upperItems一致则移动至上半屏显示区域 */}
           <View style={[styles.videoContainer, { backgroundColor: 'rgb(0, 74, 175)' }]} id="upperitemsId">
           </View>
           // ...
           {/* 子组件id与父组件upperItems不一致则移动至下半屏显示区域 */}
      </FolderStack>

      图5-10 FolderStack

      2. FoldSplitContainer组件

      FoldSplitContainer分栏布局,实现折叠屏二分栏、三分栏在展开态、悬停态以及折叠态的区域控制。

      const TestSample = () => {
          const primaryRender = () => (
          // ...FoldSplitContainer 主要区域
          );
          const secondRender = () => (
          // ...FoldSplitContainer 次要区域
          );
          const extraRender = () => (
          // ...FoldSplitContainer 扩展区域
          );
          const expandedLayoutOptions: ExpandedRegionLayoutOptions = {
          // ...折叠屏展开态配置
          };
          const hoverModeRegionLayoutOptions: HoverModeRegionLayoutOptions = {
          // ...折叠屏悬停态配置
          };
         const foldedRegionLayoutOptions: FoldedRegionLayoutOptions = {
          // ...折叠屏折叠态配置
          };
          return (
             <View style={styles.container}>
                 // 折叠屏高阶组件
                 <FoldSplitContainer
                      //主要区域回调
                      primary={primaryRender()}
                      //次要区域回调
                      secondary={secondRender()}
                     //拓展区域回调
                      extra={extraRender()}
                     //展开态布局信息
                      expandedLayoutOptions={expandedLayoutOptions}
                     //悬停态布局信息
                      hoverModeLayoutOptions={hoverModeRegionLayoutOptions}
                     //悬停态布局信息
                      foldedLayoutOptions={foldedRegionLayoutOptions}
                     //当悬停状态改变的时候回调。
                      onHoverStatusChange={(HoverStatus) => {}
                  />
             </View>
              );
      };

        图5-11 FoldSplitContainer

        • Flutter适配指导

        在使用 FoldSplitContainer 和 FolderStack 组件时,需要根据不同的折叠状态(展开、半折叠、折叠)进行布局适配。需注意这两个组件必须占用整个屏幕,否则位置会计算失败。

        1. 导入区域避让功能库

        import 'package:hadss_avoid_area/hadss_avoid_area.dart';

        2. FoldSplitContainer 可以通过配置不同的布局选项,如 FoldedRegionLayoutOptions、ExpandedRegionLayoutOptions 和 HoverModeRegionLayoutOptions,来实现不同状态下的布局效果。

        /// packages/avoid_area/example/lib/fold_split_container_ui.dart
        class _MyAppState extends State<FoldSplitContainerUI> {
          // 展开态布局信息
          ExpandedRegionLayoutOptions expandedRegionLayoutOptions =
              ExpandedRegionLayoutOptions(
                  horizontalSplitRatio: PresetSplitRatio.layout_3V2,
                  verticalSplitRatio: PresetSplitRatio.layout_1V1,
                  isExtraRegionPerpendicular: true,
                  extraRegionPosition: ExtraRegionPosition.top);
          // 折叠态布局信息
          HoverModeRegionLayoutOptions foldingRegionLayoutOptions =
              HoverModeRegionLayoutOptions(
                  horizontalSplitRatio: PresetSplitRatio.layout_3V2,
                  showExtraRegion: false,
                  extraRegionPosition: ExtraRegionPosition.top);
          // 悬停态布局信息
          FoldedRegionLayoutOptions foldedRegionLayoutOptions =
              FoldedRegionLayoutOptions(
                  verticalSplitRatio: PresetSplitRatio.layout_1V1);
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
                body: FoldSplitContainer(
                primary: Container(
                  // 主要区域布局
                ),
                secondary: Container(
                  // 次要区域布局
                ),
                extra: Container(
                  // 扩展区域布局,不传入的情况,没有对应区域
                ),
                // 折叠态布局信息
                foldedLayoutOptions: foldedRegionLayoutOptions,
                // 展开态布局信息
                expandedLayoutOptions: expandedRegionLayoutOptions,
                // 悬停态布局信息
                hoverModeLayoutOptions: foldingRegionLayoutOptions,
              ));
          }
        }

        参考链接:fold_split_container_ui.dart

        3. FolderStack 在悬停状态通过识别 upperItems 自动避让折叠屏折痕区后移到上半屏,上方为显示区下方为操作区。

        /// packages/avoid_area/example/lib/folder_stack_ui.dart
        FolderStack(
          // 配置item1和item2悬停状显示在上半屏
          upperItems: const ["item1", "item2"],
          onFolderStateChange: (state) {
            if (kDebugMode) {
              print("state=$state");
            }
          },
          children: [
            Container(
              // item1的Key配置
              key: const ValueKey('item1'),
              width: double.infinity,
              height: double.infinity,
              color: Colors.red,
              child: const Center(
                child: Text("背景"),
              ),
            ),
            Container(
              // item2的Key配置
              key: const ValueKey('item2'),
              color: Colors.blue,
              width: double.infinity,
              margin: const EdgeInsets.all(20),
              height: 30,
              child: const Text(
                "弹幕",
                textAlign: TextAlign.center,
              ),
            ),
            Container(
              color: Colors.yellow,
              child: const Text("标题,返回"),
            ),
            Align(
              alignment: Alignment.bottomCenter,
              child: Container(
                width: double.infinity,
                height: 100,
                color: Colors.grey,
                alignment: Alignment.center,
                child: const Text(
                  "进度条",
                  textAlign: TextAlign.center,
                ),
              ),
            )
          ]
        )

        参考链接:folder_stack_ui.dart

        四、示例代码

        开发者可以通过以上地址查看完整的区域避让示例代码和折叠屏布局组件接入指导,并根据自己的需求进行开发、修改和扩展。

        Logo

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

        更多推荐