如何在鸿蒙系统上实现「沉浸式」页面?

0 阅读5分钟

兄弟们圣诞快乐 🎉

何谓「沉浸式」体验?

相信 Android or iOS应用开发者对「沉浸式」一词都不陌生。所谓的沉浸式,通常就是指应用将内容区延伸到状态栏和导航栏界面,使得用户使用时更加专注于内容,不被无关元素干扰。

画板

顾名思义,应用使用沉浸式设计能带来以下几点好处:

  • 使页面和避让区域的色调统一,为用户提供更好的视觉体验。
  • 最大程度利用屏幕可视区域,使页面获得更大的布局空间。
  • 提供完全沉浸的体验,让用户沉浸其中,不被其他事物所干扰。

如何实现沉浸式设计?

主流的移动操作系统都提供了相关能力以便开发者实现沉浸式设计,主要包含以下几点能力:

  • 扩展内容的展示区域至状态栏和导航栏部分,将应用的内容区延伸到状态栏和导航栏,充分利用屏幕的可视区域;
  • 处理内容区域(安全区)状态栏以及导航栏(非安全区)内容展示的避让逻辑;
  • 将状态栏和导航条颜色与界面元素颜色相匹配,不出现明显的突兀感。

鸿蒙系统同样也提供了以上能力,我们一起来看一下。

将应用页面延伸到状态栏和导航栏

Harmony OS系统上,想要最大程度利用屏幕可视区域,将应用页面延伸到状态栏和导航栏区域,使页面获得更大的布局空间,有以下两种方法:

  1. 使用 Window.setWindowLayoutFullScreen() 方法设置窗口为全屏模式。

  2. 设置组件的expandSafeArea属性,扩展组件的安全区域到状态栏和导航栏,从而实现沉浸式。

两种方式都能将应用页面展示区域延伸至状态栏和导航栏区域,不同的是expandSafeAreaapi只作用于当前组件,只会将当前组件延伸到状态栏和导航栏,不影响其他组件的布局范围,其他组件仍在安全区域内进行布局;

Window.setWindowLayoutFullScreen()api 会将页面的所有组件布局范围从安全区域扩展为整个窗口(包括状态栏和导航栏),以电商首页为例,两种方式差异性如下图。

画板

避让安全区域

使用expandSafeArea属性无需避让安全区域

使用expandSafeArea属性扩展背景组件安全区域时,只会将当前组件延伸到状态栏和导航栏,不影响其他组件的布局范围,其他组件仍在安全区域内进行布局,所以不需要状态栏以及导航栏进行避让:

Tabs({ barPosition: BarPosition.End }) {
  // ...
}
.backgroundColor('#F1F3F5')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

使用Window.setWindowLayoutFullScreen()需对非安全区域进行避让

使用Window.setWindowLayoutFullScreen()方法设置应用窗口为全屏后,应用的页面将延伸到状态栏和导航栏,我们需要通过Window.getWindowAvoidArea()方法获取状态栏和导航栏高度,并用状态变量avoidArea记录。使用avoidAreaChange事件监听避让区域的变化,变化时更新状态变量avoidArea

@State avoidArea: AvoidArea = { topRectHeight: 0, bottomRectHeight: 0 };

onShown() {
  this.windowClass.setWindowLayoutFullScreen(true);
  this.setAvoidArea()
  this.windowClass.on('avoidAreaChange', this.onAvoidAreaChange)
}

// ...

setAvoidArea() {
  // status bar area
  const statusBarArea = this.windowClass.getWindowAvoidArea(statusBarType);
  this.avoidArea.topRectHeight = statusBarArea.topRect.height;
  // navigation bar area
  const navBarArea = this.windowClass.getWindowAvoidArea(navBarType);
  this.avoidArea.bottomRectHeight = navBarArea.bottomRect.height;
}

onAvoidAreaChange = (data: window.AvoidAreaOptions) => {
  if (data.type === statusBarType) {
    this.avoidArea.topRectHeight = data.area.topRect.height;
  } else if (data.type === navBarType) {
    this.avoidArea.bottomRectHeight = data.area.bottomRect.height;
  }
}

获得状态栏和导航栏的高度后,再对需要进行避让的组件添加上下padding,上下padding分别为状态栏和导航栏的高度,达到内容避让状态栏和导航条的效果:

Tabs({ barPosition: BarPosition.End }) {
  // ...
}
.backgroundColor('#F1F3F5')
.padding({
  top: this.avoidArea.topRectHeight + 'px',
  bottom: this.avoidArea.bottomRectHeight + 'px'
})

隐藏状态栏/导航栏

某些场景下,我们希望全身心沉浸式使用app,eg:全身心沉浸式打王者、追剧看电影、阅读。此时,我们往往会觉得导航栏和状态栏很「碍眼」,想要隐藏状态栏和导航栏,在使用Window.setWindowLayoutFullScreen()方法设置窗口为全屏模式后,我们还需要调用Window.setWindowSystemBarEnable()方法设置状态栏和导航条的显隐:

onShown() {
  this.windowClass.setWindowLayoutFullScreen(true);
  this.windowClass.setWindowSystemBarEnable([]);
}

onHidden() {
  this.windowClass.setWindowLayoutFullScreen(false);
  this.windowClass.setWindowSystemBarEnable(['status', 'navigation']);
}

build() {
  NavDestination() {
    Column() {
      Video({ src: $rawfile('video.mp4') })
        // ...
    }
    .height('100%')
    .width('100%')
  }
  .hideTitleBar(true)
  .onShown(() => this.onShown())
  .onHidden(() => this.onHidden())
}

鸿蒙系统目前对于隐藏状态栏的实现是将状态栏整体布局隐藏,因此状态栏高度也会变为0,这点和Android上的表现也是不一样的。

需要注意的是,目前的鸿蒙设备大多采用挖孔屏幕,我们在隐藏状态栏和导航栏后需要考虑挖孔区域是否对交互会产生影响,决定是否要对挖孔区域进行避让,以保证用户正常的交互体验,获取挖孔区域的宽高和位置可以调用以下api:

// 通过Display.getCutoutInfo()方法获取挖孔区域的宽高和位置信息。
async getCutoutInfo() {
  const displayClass = display.getDefaultDisplaySync();
  const res = await displayClass.getCutoutInfo();
  return res.boundingRects;
}

画板

状态栏颜色适配

在某些将深色背景延伸到状态栏的沉浸式页面中,我们需要根据页面内状态栏区域的背景色选择合适的状态栏颜色 (黑/白),保证易读性。eg:状态栏背景为深色时,需要设置状态栏时间文字、信号图标、电量图标等内容为浅色进行适配,避免状态栏内容不清晰,以此提升用户的视觉体验:

画板

我们可以使用Window.setWindowSystemBarProperties()方法设置状态栏内容的颜色:

onShown() {
  this.windowClass.setWindowSystemBarProperties({
    statusBarContentColor: '#FFFFFF'
  });
}

onHidden() {
  this.windowClass.setWindowSystemBarProperties({
    statusBarContentColor: '#000000'
  });
}

build() {
  NavDestination() {
    // ...
  }
  .hideTitleBar(true)
  .onShown(() => this.onShown())
  .onHidden(() => this.onHidden())
}