鸿蒙UI开发——基于组件安全区方案实现沉浸式界面

177 阅读4分钟

1、概 述

本文是接着上篇文章 鸿蒙UI开发——基于全屏方案实现沉浸式界面 的继续讨论。除了全屏方案实现沉浸式界面外,我们还可以使用组件安全区的方案。

当我们没有使用setWindowLayoutFullScreen()接口设置窗口为全屏布局时,默认使用的策略就是组件安全区布局方式。即:

【应用在默认情况下窗口背景绘制范围是全屏,但UI元素被限制在安全区内(自动排除状态栏和导航条)进行布局,来避免界面元素被状态栏和导航条遮盖】

示意图如下:

image.png

我们将按业务场景去分析基于组件安全区的沉浸式界面的开发。

2、状态栏和导航条颜色相同场景

针对有些APP,状态栏和导航条颜色可能相同,例如如下效果:

image.png

针对这种场景,我们可以通过设置窗口的背景色来实现沉浸式效果,设置方式为:setWindowBackgroundColor() 【将窗口的颜色和应用的颜色设置为一致】

Ability中设置window的背景色代码如下(请注意14行代码):

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  // ...

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        return;
      }

      // 设置全窗颜色和应用元素颜色一致
      windowStage.getMainWindowSync().setWindowBackgroundColor('#008000');
    });
  }
}

组件中的背景色设置代码如下(请注意35行代码):

// xxx.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('ROW1').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW2').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW3').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW4').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW5').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW6').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.SpaceBetween)
    .backgroundColor('#008000')
  }
}

实现效果如下:

image.png

3、状态栏和导航条颜色不同场景

状态栏和导航条颜色不同时,可以使用expandSafeArea属性扩展安全区域属性进行调整。示例代码如下:

// xxx.ets
@Entry
@Component
struct Example {
  build() {
    Column() {
      Row() {
        Text('Top Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor('#F08080')
      // 设置顶部绘制延伸到状态栏
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])

      Row() {
        Text('ROW2').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW3').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW4').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW5').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('Bottom Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor(Color.Orange)
      // 设置底部绘制延伸到导航条
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .width('100%').height('100%').alignItems(HorizontalAlign.Center)
    .backgroundColor('#008000')
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

效果如下:

image.png

【扩展安全区域属性原理】

  • 布局阶段按照安全区范围大小进行UI元素布局。
  • 布局完成后查看设置了expandSafeArea的组件边界(不包括margin)是否和安全区边界相交。
  • 如果设置了expandSafeArea的组件和安全区边界相交,根据expandSafeArea传递的属性则进一步扩大组件绘制区域大小覆盖状态栏、导航条这些非安全区域
  • 上述过程仅改变组件自身绘制大小,不进行二次布局,不影响子节点和兄弟节点的大小和位置
  • 子节点可以单独设置该属性,只需要自身边界和安全区域重合就可以延伸自身大小至非安全区域内,需要确保父组件未设置clip等裁切属性。
  • 配置expandSafeArea属性组件进行绘制扩展时,需要关注组件不能配置固定宽高尺寸,百分比除外。

4、背景图和视频场景

设置背景图、视频控件大小为安全区域大小并配置expandSafeArea属性。

// xxx.ets
@Entry
@Component
struct SafeAreaExample1 {
  build() {
    Stack() {
      Image($r('app.media.bg'))
        .height('100%').width('100%')
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) // 图片组件的绘制区域扩展至状态栏和导航条。
    }.height('100%').width('100%')
  }
}

效果图如下:

image.png

5、滚动类场景

场景需求:需要List滚动类组件滚动过程中元素可以和导航条重合,滚动至底部时,元素在导航条上面需要避让。示意图如下:

image.png

由于expandSafeArea不改变子节点布局,因此,List等滚动类组件可以调用expandSafeArea,延伸List组件视图窗口大小而不改变ListItem内在布局。实现ListItem在滑动过程中显示在导航条下,但滚动至最后一个时显示在导航条上。

开发示例如下:

step 1 : 配置窗口整体底色

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  // ...

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        return;
      }

      windowStage.getMainWindowSync().setWindowBackgroundColor('#DCDCDC'); // 配置窗口整体底色
    });
  }
}

step 2 : 界面代码展示

// xxx.ets
@Entry
@Component
struct ListExample {
  private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  build() {
    Column() {
      List({ space: 20, initialIndex: 0 }) {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text('' + item)
              .width('100%')
              .height(100)
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .borderRadius(10)
              .backgroundColor(0xFFFFFF)
          }
        }, (item: string) => item)
      }
      .listDirection(Axis.Vertical) // 排列方向
      .scrollBar(BarState.Off)
      .friction(0.6)
      .divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
      .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
      .width('90%')
      // List组件的视窗范围扩展至导航条。
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }
    .width('100%')
    .height('100%')
    .padding({ top: 15 })
  }
}

6、底部页签场景

场景需求:页签背景色能够延伸到导航条区域,但页签内部可操作元素需要在导航条之上。

image.png

针对底部的页签部分,Navigation组件和Tabs组件默认实现了页签的延伸处理,我们使用时只需要保证Navigation和Tabs组件的底部边界和底部导航条重合即可。

若我们显式调用expandSafeArea接口,则安全区效果由expandSafeArea参数指定。

如果我们没有使用Navigation和Tab组件而是采用自定义方式实现页签的场景,可以针对底部元素设置expandSafeArea属性实现底部元素的背景扩展。

顶部和底部UI元素未设置和设置expandSafeArea属性效果对比如下:

image.png

沉浸式实现代码如下:

// xxx.ets
@Entry
@Component
struct VideoCreateComponent {
  build() {
    Column() {
      Row() {
        Text('Top Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor('#F08080')
      // 设置顶部绘制延伸到状态栏
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])

      Row() {
        Text('ROW2').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW3').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW4').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('ROW5').fontSize(40)
      }.backgroundColor(Color.Orange).padding(20)

      Row() {
        Text('Bottom Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
      }
      .backgroundColor(Color.Orange)
      // 设置底部绘制延伸到导航条
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }
    .width('100%').height('100%').alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.SpaceBetween)
    .backgroundColor(Color.Green)
  }
}

7、图文场景

当状态栏元素和底部导航条元素不同时,无法单纯通过窗口背景色或者背景图组件延伸实现,此时需要对顶部元素和底部元素分别配置expandSafeArea属性,顶部元素配置expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.TOP]),底部元素配置expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.BOTTOM])。

代码如下:

@Entry
@Component
struct Index {
  build() {
    Swiper() {
      Column() {
        Image($r('app.media.start'))
          .height('50%').width('100%')
          // 设置图片延伸到状态栏
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
        Column() {
          Text('HarmonyOS 第一课')
            .fontSize(32)
            .margin(30)
          Text('通过循序渐进的学习路径,无经验和有经验的开发者都可以掌握ArkTS语言声明式开发范式,体验更简洁、更友好的HarmonyOS应用开发旅程。')
            .fontSize(20).margin(20)
        }.height('50%').width('100%')
        .backgroundColor(Color.White)
        // 设置文本内容区背景延伸到导航栏
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
      }
    }
    .width('100%')
    .height('100%')
    // 关闭Swiper组件默认的裁切效果以便子节点可以绘制在Swiper外。
    .clip(false)
  }
}

实现效果如下:

image.png