Table of Contents

一、安全区域

安全区域定义为页面的显示区域,其默认不与系统设置的非安全区域(如状态栏、导航栏)重叠,以确保开发者设计的界面均布局于安全区域内。然而,当Web组件启用沉浸式模式时,网页元素可能会出现与状态栏或导航栏重叠的问题。具体示例如图1所示,中间部分的区域即为安全区域,而顶部状态栏、屏幕挖孔区域和底部导航条则被界定为避让区,Web组件开启沉浸式效果时,网页内底部元素与导航条发生重叠。

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_软键盘
提供属性方法允许开发者设置组件绘制内容突破安全区域的限制,通过expandSafeArea属性支持组件不改变布局情况下扩展其绘制区域至安全区外,通过设置setKeyboardAvoidMode来配置虚拟键盘弹出时页面的避让模式。页面中有标题栏等文字不希望和非安全区重叠时,建议对组件设置expandSafeArea属性达到沉浸式效果,也可以直接通过窗口接口setWindowLayoutFullScreen设置沉浸式。

说明

默认摄像头挖孔区域不为非安全区域,页面不避让挖孔。
从API Version 12开始,可在module.json5中添加配置项, 摄像头挖孔区域视为非安全区,实现页面默认避让挖孔:

"metadata": [
{
"name": "avoid_cutout",
"value": "true",
}
],
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

二、expandSafeArea属性

控制组件扩展其安全区域。

expandSafeArea(types?: Array<SafeAreaType>, edges?: Array<SafeAreaEdge>)
  • 1.

三、软键盘避让模式

当用户在输入时,为了确保输入框不会被键盘遮挡,系统提供了避让模式来解决这一问题。开发者可以通过setKeyboardAvoidMode控制虚拟键盘抬起时页面的避让模式,避让模式有上抬模式和压缩模式两种,键盘抬起时默认页面避让模式为上抬模式。

1、设置虚拟键盘抬起时页面的避让模式
setKeyboardAvoidMode(value: KeyboardAvoidMode): void
  • 1.
2、获取虚拟键盘抬起时的页面避让模式
getKeyboardAvoidMode(): KeyboardAvoidMode

  • 1.
  • 2.

四、相关示例

1、实现沉浸式效果

通过设置expandSafeArea属性向顶部和底部扩展安全区实现沉浸式效果。

// xxx.ets
@Entry
@Component
struct SafeAreaExample1 {
  build() {
    Column() {
      // ......
    }
    .height('100%')
    .width('100%')
    .backgroundImage($r('app.media.bg'))
    .backgroundImageSize(ImageSize.Cover)
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_软键盘_02

2、滚动列表底部延伸场景

在列表滚动场景中,滚动时内容可与导航条区域重合,滚动到底部时,底部内容需避让导航条。

设置列表组件List组件的expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]),扩展列表底部到安全区域。此时List组件显示区域扩大,滚动时列表内容可在导航条区域显示。

// FaqList.ets

@Entry
@Component
struct FaqListPage{
  listData: string[] = ['问题1', '问题2', '问题3', '问题4', '问题5', '问题6', '问题7', '问题8', '问题9'];

  build() {
    Column({ space: 10 }) {
      Row() {
        Text('常见问题')
          .fontSize(22)
      }
      .height(100)

      List({ space: 10 }) {
        ForEach(this.listData, (item: string) => {
          ListItem() {
            Column({ space: 10 }) {
              Text(item)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
              Text(item + '内容')
                .fontSize(16)
                .fontColor('#999999')
            }
            .alignItems(HorizontalAlign.Start)
            .padding(15)
            .width('100%')
            .height(150)
            .borderRadius(10)
            .backgroundColor(Color.White)
          }
        }, (item: string) => item)
        ListItem() {
          Text('已加载全部')
            .width('100%')
            .textAlign(TextAlign.Center)
            .opacity(0.6)
            .padding(10)
        }
      }
      .padding(10)
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .backgroundColor('#f1f3f5')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

将滚动到底部的提示添加在列表项末尾,由于设置expandSafeArea属性不影响子组件的布局,所以滚动到底部时提示文字默认会避让导航条。

常见效果运行效果
鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_软键盘_03
鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_插入图片_04
3、重要信息被软键盘遮挡

例如下面这个电子邮件的示例,内容由三部分组成:标题栏、内容区域和底部操作栏。当点击输入内容的输入框,软键盘会挡住底部的操作栏,影响用户体验,如下图所示:

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_javascript_05

开发者可以通过设置软键盘的避让模式为KeyboardAvoidMode.RESIZE(压缩模式),来解决底部操作栏被遮挡的问题,设置该属性后,软键盘的避让会通过压缩内容区域的高度来实现。示例代码如下:

import { KeyboardAvoidMode } from '@kit.ArkUI';

@Entry
@Component
struct MailPage {

  aboutToAppear(): void {
    this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
  }

  aboutToDisappear(): void {
    this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET);
  }

  build() {
    Column() {
      // 标题栏
      this.NavigationTitle()
      // 内容区域
      this.EmailContent()
      // 操作栏
      this.BottomToolbar()
    }
    .height('100%')
    .width('100%')
    .backgroundColor('#f1f3f5')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }

  @Builder
  NavigationTitle() {
    Row() {
      Image($r('app.media.arrow_left'))
        .width(24)
        .height(24)
        .margin({ right: 16 })

      Text('新建电子邮件')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Blank()

      Image($r('app.media.paperplane'))
        .width(24)
        .height(24)

    }
    .width('100%')
    .height(56)
    .padding({
      left: 24,
      right: 24
    })
  }

  @Builder
  EmailContent() {
    Column() {
      this.RowInfo('发件人')
      this.RowInfo('收件人')
      this.RowInfo('主题')
      Row() {
        TextArea({ placeholder: '请输入邮件正文' })
          .height('100%')
          .backgroundColor('#f1f3f5')
      }
      .layoutWeight(1)
      .alignItems(VerticalAlign.Top)
      .width('100%')
      .margin({ top: 12 })
    }.width('100%')
    .layoutWeight(1)
    .padding({ left: 24, right: 24 })
    .margin({ top: 8 })
  }

  @Builder
  RowInfo(param: string) {
    Row() {
      Text(`${param}`)
        .fontColor('#6f7780')
        .fontSize(16)
      TextInput({ placeholder: `请输入${param}` })
        .width('100%')
        .backgroundColor('#f1f3f5')
    }
    .width('100%')
    .height(48)
    .border({
      width: { top: 1 },
      color: '#e8ebed'
    })
  }

  @Builder
  BottomToolbar() {
    Row({ space: 24 }) {
      Image($r('app.media.folder'))
        .ImageSize()
      Image($r('app.media.picture'))
        .ImageSize()
      Image($r('app.media.arrow_up_circle'))
        .ImageSize()
      Image($r('app.media.share2'))
        .ImageSize()
    }
    .width('100%')
    .height(56)
    .padding({ left: 24, right: 24 })
    .border({
      width: { top: 1 },
      color: '#E8EBED'
    })
  }

  @Styles
  ImageSize() {
    .height(24)
    .width(24)
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_javascript_06

4、软键盘弹出导致布局错位

例如下面这样的一个聊天界面,顶部是一个自定义的标题,下方为可滚动聊天消息区域,底部是消息输入框。但是由于软键盘避让默认是上抬模式,会把整个页面向上抬起,所以标题也会被顶上去,如下图所示。现在需求希望顶部标题固定,点击底部输入框软键盘弹起的时候,标题不上抬,只有内容区域上抬。

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_插入图片_07

需要给对应的组件设置 .expandSafeArea([SafeAreaType.KEYBOARD])}属性,使标题组件不避让键盘,示例代码如下:

  • 先设置窗口为全屏模式。

// EntryAbility.ets

onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    // 获取该WindowStage实例下的主窗口。
    const mainWindow = windowStage.getMainWindowSync();
    // 设置主窗口的布局是否为沉浸式布局。
    mainWindow.setWindowLayoutFullScreen(true).then(() => {
      hilog.info(0x0000, 'testTag', 'Succeeded in setting the window layout to full-screen mode');
    }).catch((err: BusinessError) => {
      hilog.info(0x0000, 'testTag', 'Failed to set the window layout to full-screen mode. Cause: %{public}s', JSON.stringify(err) ?? '');
    })
    // ...
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 再设置标题组件不避让键盘
    // ContactPage.ets
@Componentexport struct ContactPage {
  build() {
    Column() {
        Row() { // 顶部自定义标题栏
          // ...
        }
        .height('12%')
        .expandSafeArea([SafeAreaType.KEYBOARD]) // 标题组件不避让键盘
        .zIndex(1)

        List() { // 聊天消息区域
          // ...
        }
        .height('76%')

        Column(){ // 底部消息输入框
          // ...
        }
        .height('12%')
    }
    .width('100%')
    .height('100%')
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_javascript_08

5、自定义弹窗被键盘顶起

在软键盘系统避让机制中介绍过,弹窗为避让软键盘会进行避让,整体向上抬,这样可能会影响用户体验。比如下面这个评论里列表的弹窗,使用@CustomDialog实现的。当用户点击弹窗底部的输入框的时候,弹窗会整体上抬,输入框上抬的距离也过多。

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_软键盘_09

为了解决以上问题,可以使用Navigation.Dialog,通过设置NavDestination的mode为NavDestinationMode.DIALOG弹窗类型,此时整个NavDestination默认透明显示,示例代码如下:

// NavDestinationModeDemo.ets

import { Chat, chatList } from '../model/CommentData';

@Entry
@Component
struct NavDestinationModeDemo {
  @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()

  @Builder
  PagesMap(name: string) {
    if (name === 'DialogPage') {
      DialogPage()
    }
  }

  build() {
    Navigation(this.pageStack) {
      Column() {
        Button('点击评论')
          .onClick(() => {
            this.pageStack.pushPathByName('DialogPage', '');
          })
      }
      .height('100%')
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .mode(NavigationMode.Stack)
    .navDestination(this.PagesMap)
  }
}

@Component
export struct DialogPage {
  @Consume('NavPathStack') pageStack: NavPathStack;

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Bottom }) {
        Column() {
          Row() {
            Text('评论')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
            Blank()
            Button() {
              Image($r('app.media.cancel'))
                .width(18)
            }
            .padding(10)
            .backgroundColor('rgba(0,0,0,0.05)')
            .onClick(() => {
              this.pageStack.pop();
            })
          }
          .padding(15)
          .width('100%')

          List({space:20}) {
            ForEach(chatList, (item: Chat) => {
              ListItem() {
                Row({space:10}) {
                  Image(item.profile)
                    .width(40)
                    .height(40)
                    .borderRadius(40)
                  Column({ space: 10 }) {
                    Text(item.nickname)
                      .fontSize(16)
                      .fontColor('#999999')
                    Text(item.content)
                      .fontSize(16)
                      .fontColor('#333333')
                  }
                  .width('100%')
                  .justifyContent(FlexAlign.Start)
                  .alignItems(HorizontalAlign.Start)
                }
                .width('100%')
                .justifyContent(FlexAlign.Start)
                .alignItems(VerticalAlign.Top)
              }
            })
          }
          .scrollBar(BarState.Off)
          .width('100%')
          .layoutWeight(1)

          TextInput({ placeholder: '写评论' })
            .height(40)
            .width('100%')
        }
        .borderRadius({
          topLeft: 32,
          topRight: 32
        })
        .backgroundColor(Color.White)
        .height('75%')
        .width('100%')
        .padding(10)
      }
      .height('100%')
      .width('100%')
    }
    .backgroundColor('rgba(0,0,0,0.2)')
    .hideTitleBar(true)
    .mode(NavDestinationMode.DIALOG)
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.

此外还需要设置软键盘避让模式为压缩模式,示例代码如下:

// EntryAbility.ets

import { KeyboardAvoidMode, window } from '@kit.ArkUI';

 onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/CustomDialogDemoPage', (err) => {
      // 设置虚拟键盘抬起时压缩页面大小为减去键盘的高度
      windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
    });
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

鸿蒙HarmonyOS NEXT开发:安全区域、沉浸式页面开发实践,软键盘布局适配解决方案_软键盘_10

一键三连+关注,你的支持是我创作的动力。在这里,我乐于倾囊相授。